├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .gitmodules
├── .importsortrc
├── .prettierrc
├── README.md
├── README_EN.md
├── apps
└── cli-app
│ ├── src
│ ├── app
│ │ ├── app.module.ts
│ │ ├── app.service.spec.ts
│ │ ├── app.service.ts
│ │ ├── index.ts
│ │ └── strategy
│ │ │ ├── index.ts
│ │ │ ├── strategy.module.ts
│ │ │ └── strategy.service.ts
│ └── main.ts
│ └── tsconfig.app.json
├── assets
└── images
│ ├── logo.svg
│ ├── logo_circle.png
│ └── logo_circle.svg
├── config
└── default.sample.toml
├── docker-compose.yml
├── gulpfile.js
├── jest.config.js
├── jest.setup.js
├── libs
├── broker-api
│ ├── binance-api
│ │ ├── binance-api.module.ts
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ ├── services
│ │ │ ├── binance-api
│ │ │ │ ├── binance-api.service.spec.ts
│ │ │ │ └── binance-api.service.ts
│ │ │ ├── binance-rest-client
│ │ │ │ ├── binance-rest-client.service.spec.ts
│ │ │ │ └── binance-rest-client.service.ts
│ │ │ ├── binance-websocket-client
│ │ │ │ ├── binance-websocket-client.service.spec.ts
│ │ │ │ └── binance-websocket-client.service.ts
│ │ │ ├── index.ts
│ │ │ └── websocket-handler
│ │ │ │ ├── websocket-handler.service.spec.ts
│ │ │ │ └── websocket-handler.service.ts
│ │ └── types
│ │ │ ├── index.ts
│ │ │ ├── order.ts
│ │ │ ├── pair.ts
│ │ │ └── ws.ts
│ ├── broker-api.module.ts
│ └── index.ts
├── common
│ ├── big-number
│ │ ├── calculations.spec.ts
│ │ ├── calculations.ts
│ │ ├── compare.spec.ts
│ │ ├── compare.ts
│ │ ├── condition.spec.ts
│ │ ├── condition.ts
│ │ ├── format.spec.ts
│ │ ├── format.ts
│ │ ├── get-big-number.spec.ts
│ │ ├── get-big-number.ts
│ │ ├── index.ts
│ │ ├── validation.spec.ts
│ │ └── validation.ts
│ ├── descriptors
│ │ ├── catch-error-decorator.spec.ts
│ │ ├── catch-error.decorator.ts
│ │ ├── index.ts
│ │ ├── log-caller.decorator.ts
│ │ ├── retry-order-decorator.spec.ts
│ │ └── retry-order.decorator.ts
│ ├── di
│ │ ├── index.ts
│ │ └── injection-token.ts
│ ├── index.ts
│ ├── rx
│ │ ├── index.ts
│ │ ├── operators.spec.ts
│ │ └── operators.ts
│ ├── time
│ │ ├── get-timestring.ts
│ │ └── index.ts
│ └── utils
│ │ ├── get-caller-method-name
│ │ ├── get-caller-method-name.spec.ts
│ │ ├── get-caller-method-name.ts
│ │ └── index.ts
│ │ ├── get-edge-order-amount
│ │ ├── get-edge-order-amount.spec.ts
│ │ ├── get-edge-order-amount.ts
│ │ └── index.ts
│ │ ├── get-triangle-rate
│ │ ├── get-triangle-rate.spec.ts
│ │ ├── get-triangle-rate.ts
│ │ └── index.ts
│ │ └── index.ts
├── config
│ ├── config.ts
│ └── index.ts
├── exceptions
│ ├── exception-handler
│ │ ├── default-exception-handler.spec.ts
│ │ ├── default-exception-handler.ts
│ │ ├── exception-handler.ts
│ │ └── index.ts
│ └── index.ts
├── logger
│ ├── common
│ │ ├── index.ts
│ │ ├── predef.ts
│ │ ├── stringify.spec.ts
│ │ ├── stringify.ts
│ │ ├── tokens.ts
│ │ ├── types.ts
│ │ └── utils
│ │ │ ├── get-log-content.ts
│ │ │ ├── index.ts
│ │ │ ├── is-allowed-log-level.ts
│ │ │ └── make-colored-log-args.ts
│ ├── index.ts
│ ├── logger.module.ts
│ ├── logger.spec.ts
│ ├── logger.ts
│ └── strategy
│ │ ├── console-log-strategy.spec.ts
│ │ ├── console-log-strategy.ts
│ │ ├── index.ts
│ │ ├── log-strategy.ts
│ │ └── multi-log-strategy.ts
├── models
│ ├── index.ts
│ ├── strategy
│ │ ├── index.ts
│ │ ├── strategy.ts
│ │ ├── tokens.ts
│ │ └── trading-strategy.ts
│ └── trade.ts
├── modules
│ ├── data
│ │ ├── data.module.ts
│ │ ├── data.service.spec.ts
│ │ ├── data.service.ts
│ │ └── index.ts
│ ├── engine
│ │ ├── engine.module.ts
│ │ ├── engine.service.spec.ts
│ │ ├── engine.service.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── shared
│ │ ├── index.ts
│ │ ├── on-destroy
│ │ │ ├── index.ts
│ │ │ ├── on-destroy.service.spec.ts
│ │ │ └── on-destroy.service.ts
│ │ └── shared.module.ts
│ └── trade
│ │ ├── index.ts
│ │ ├── trade.module.ts
│ │ ├── trade.service.spec.ts
│ │ └── trade.service.ts
├── notifications
│ ├── email
│ │ ├── email-handle.spec.ts
│ │ ├── email-handle.ts
│ │ ├── email.template.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── notification-manager.spec.ts
│ └── notification-manager.ts
├── persistence
│ ├── common
│ │ ├── constants.ts
│ │ ├── get-connection-options.ts
│ │ ├── index.ts
│ │ ├── testing
│ │ │ ├── entity-test-bed
│ │ │ │ ├── entity-test-bed.spec.ts
│ │ │ │ ├── entity-test-bed.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── test-helpers
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── override-timestamp-columns.spec.ts
│ │ │ │ │ └── override-timestamp-columns.ts
│ │ │ ├── index.ts
│ │ │ ├── mock.data.ts
│ │ │ └── test-helpers
│ │ │ │ ├── export-as-json-file.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── override-timestamp-columns.spec.ts
│ │ │ │ └── override-timestamp-columns.ts
│ │ ├── transformers.spec.ts
│ │ └── transformers.ts
│ ├── entity
│ │ ├── index.ts
│ │ ├── trade-edge
│ │ │ ├── index.ts
│ │ │ ├── trade-edge.entity.ts
│ │ │ ├── trade-edge.repository.spec.ts
│ │ │ └── trade-edge.repository.ts
│ │ └── trade-triangle
│ │ │ ├── index.ts
│ │ │ ├── trade-triangle.entity.ts
│ │ │ ├── trade-triangle.repository.spec.ts
│ │ │ └── trade-triangle.repository.ts
│ ├── index.ts
│ └── persistence.module.ts
└── testing
│ ├── index.ts
│ ├── mock
│ ├── data
│ │ ├── index.ts
│ │ ├── mock-order.ts
│ │ ├── mock-pair-fees.ts
│ │ ├── mock-pairs.ts
│ │ └── mock-user-data.ts
│ ├── index.ts
│ ├── mock.data.ts
│ ├── mock.module.ts
│ └── mock.service.ts
│ └── utils
│ ├── build-defer-init-service.ts
│ ├── get-dummy-execution-context.ts
│ └── index.ts
├── nest-cli.json
├── ormconfig.js
├── package.json
├── pm2.config.js
├── tools
└── gulp
│ ├── function.ts
│ ├── gulpfile.ts
│ └── tsconfig.json
├── tsconfig.build.json
├── tsconfig.json
├── webpack
├── deploy.config.js
├── migrations.config.js
└── typeorm-cli.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.js
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
9 | root: true,
10 | env: {
11 | node: true,
12 | jest: true,
13 | },
14 | ignorePatterns: ['.eslintrc.js'],
15 | rules: {
16 | "prettier/prettier": [
17 | "error",
18 | {
19 | "endOfLine": "auto"
20 | },
21 | ],
22 | 'no-empty-function': 'off',
23 | '@typescript-eslint/no-empty-function': 'off',
24 | '@typescript-eslint/ban-types': [
25 | 'error',
26 | {
27 | types: {
28 | Object: {
29 | message: 'Avoid using the `Object` type. Did you mean `object`?',
30 | },
31 | Boolean: {
32 | message: 'Avoid using the `Boolean` type. Did you mean `boolean`?',
33 | },
34 | Number: {
35 | message: 'Avoid using the `Number` type. Did you mean `number`?',
36 | },
37 | String: {
38 | message: 'Avoid using the `String` type. Did you mean `string`?',
39 | },
40 | Symbol: {
41 | message: 'Avoid using the `Symbol` type. Did you mean `symbol`?',
42 | },
43 | Function: false,
44 | },
45 | },
46 | ],
47 | '@typescript-eslint/consistent-type-definitions': 'off',
48 | '@typescript-eslint/dot-notation': 'off',
49 | '@typescript-eslint/explicit-member-accessibility': [
50 | 'off',
51 | {
52 | accessibility: 'explicit',
53 | },
54 | ],
55 | '@typescript-eslint/member-delimiter-style': [
56 | 'off',
57 | {
58 | multiline: {
59 | delimiter: 'none',
60 | requireLast: true,
61 | },
62 | singleline: {
63 | delimiter: 'semi',
64 | requireLast: false,
65 | },
66 | },
67 | ],
68 | '@typescript-eslint/member-ordering': [
69 | 'error',
70 | {
71 | default: ['static-field', 'instance-field', 'static-method', 'instance-method'],
72 | },
73 | ],
74 | '@typescript-eslint/naming-convention': 'off',
75 | '@typescript-eslint/no-empty-interface': 'off',
76 | '@typescript-eslint/no-non-null-assertion': 'off',
77 | '@typescript-eslint/no-unused-expressions': 'off',
78 | '@typescript-eslint/promise-function-async': 'off',
79 | '@typescript-eslint/quotes': [
80 | 'off',
81 | 'single',
82 | {
83 | allowTemplateLiterals: true,
84 | },
85 | ],
86 | '@typescript-eslint/semi': ['off', null],
87 | '@typescript-eslint/type-annotation-spacing': 'off',
88 | 'arrow-parens': ['off', 'always'],
89 | 'brace-style': ['off', 'off'],
90 | 'eol-last': 'off',
91 | 'import/no-deprecated': 'off',
92 | 'import/no-internal-modules': [
93 | 'off',
94 | {
95 | allow: ['@angular/*', 'aws-sdk/*', 'rxjs/*', 'zone.js/*'],
96 | },
97 | ],
98 | 'import/order': 'off',
99 | 'jsdoc/check-alignment': 'off',
100 | 'jsdoc/newline-after-description': 'off',
101 | 'jsdoc/no-types': 'off',
102 | 'linebreak-style': 'off',
103 | 'max-len': 'off',
104 | 'new-parens': 'off',
105 | 'newline-per-chained-call': 'off',
106 | 'no-console': ['error', { allow: ['log', 'warn', 'error', 'info'] }],
107 | 'no-extra-semi': 'off',
108 | 'no-invalid-regexp': 'error',
109 | 'no-irregular-whitespace': 'off',
110 | 'no-octal': 'error',
111 | 'no-octal-escape': 'error',
112 | 'no-regex-spaces': 'error',
113 | 'no-restricted-syntax': ['error', 'ForInStatement'],
114 | 'no-trailing-spaces': 'off',
115 | 'no-underscore-dangle': 'off',
116 | 'prefer-arrow/prefer-arrow-functions': 'off',
117 | 'quote-props': 'off',
118 | 'react/jsx-curly-spacing': 'off',
119 | 'react/jsx-equals-spacing': 'off',
120 | 'react/jsx-tag-spacing': [
121 | 'off',
122 | {
123 | afterOpening: 'allow',
124 | closingSlash: 'allow',
125 | },
126 | ],
127 | 'react/jsx-wrap-multilines': 'off',
128 | 'space-before-function-paren': 'off',
129 | 'space-in-parens': ['off', 'never'],
130 | },
131 | };
132 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
35 |
36 | # config
37 | /config/default.toml
38 | /config/test.toml
39 | /tsconfig.build.tsbuildinfo
40 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "apps/pro-cli-app"]
2 | path = apps/pro-cli-app
3 | url = https://github.com/zlq4863947/ta2-pro-cli-app.git
4 | branch = main
5 |
--------------------------------------------------------------------------------
/.importsortrc:
--------------------------------------------------------------------------------
1 | ".ts": {
2 | "parser": "typescript",
3 | "style": "module-alias",
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine":"auto",
3 | "printWidth": 140,
4 | "tabWidth": 2,
5 | "useTabs": false,
6 | "semi": true,
7 | "singleQuote": true,
8 | "trailingComma": "all",
9 | "bracketSpacing": true,
10 | "arrowParens": "always",
11 | "parser": "typescript"
12 | }
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # triangular-arbitrage2
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
开源的自动化三角套利交易程序。
10 | 前一版本程序地址: https://github.com/zlq4863947/triangular-arbitrage
11 |
12 | see the English
13 |
14 | ## 技术架构
15 |
16 | - 开发语言: [typescript](https://github.com/microsoft/TypeScript)
17 | - 技术库: [rxjs](https://github.com/ReactiveX/rxjs) 、[nestjs](https://github.com/nestjs/nest)
18 | - 数字货币相关库: [ccxt](https://github.com/ccxt/ccxt) 、[binance](https://github.com/tiagosiebler/binance)
19 | - 测试库: [jest](https://github.com/facebook/jest)
20 | - 虚拟环境容器: docker
21 | - 数据库: mysql 8.0
22 | - 依赖管理工具: [yarn](https://github.com/yarnpkg/yarn) (不是npm)
23 |
24 | ## 免责声明
25 |
26 | - `triangular-arbitrage2` 不是万无一失、百分之百、一定盈利交易机器人。需要您自行担风险使用它。
27 | - 数字货币仍处于萌芽实验阶段,`triangular-arbitrage2`也是如此。这意味着,两种都可能随时失败。
28 | - `triangular-arbitrage2`分为 `Basic`(免费版) 和 `Pro`(付费版)。
29 | - `Basic`(免费版),没有真实交易功能,您需要自行增加此功能,并承担由此产生的风险。
30 | - `Pro`(付费版),本人负责保证和维护此机器人的策略及交易的准确无误,不保证百分比盈利,交易产生的风险需自行承担。
31 | - 切勿让机器人长时间不受监控。 `triangular-arbitrage2` 并不知道什么时候需要停止,所以如果发生太多损失或出现问题,请准备好停止它。
32 |
33 | ## 机器人说明
34 |
35 | | | Basic(免费版) | Pro(付费版) |
36 | |--|--|--|
37 | | 开源 | ○ | △ |
38 | | 命令行应用 | ○ | ○ |
39 | | 支持的交易所 | 1(币安) | 1+n (币安+其他交易所) |
40 | | 多种类日志记录 | ○ | ○ |
41 | | 自动计算交易费率 | ○ | ○ |
42 | | 模拟交易 | ○ | ○ |
43 | | 真实交易 | × | ○ |
44 | | 收益报表| × | ○ |
45 | | 多种执行策略 | × | ○ |
46 | | 维护与支援 | github论坛 | 实时 |
47 |
48 | - 付费版可通过以下方式联系咨询
49 | - email: zlq4863947@gmail.com
50 | - qq: 442540141
51 |
52 | ## 快速开始
53 |
54 | ### 1,环境安装
55 |
56 | - 安装nodejs(最新版就可以)
57 | - 执行 `npm install` (安装程序依赖)
58 |
59 | ### 2、设置配置文件
60 |
61 | - **config/default.sample.toml**更改为**config/default.toml**
62 | - 自己修改成想要的配置,例如:币安的apikey
63 |
64 | ### 3、启动命令行机器人
65 |
66 | #### 免费版命令行应用
67 |
68 | - `npm run start:cli`
69 |
70 | #### 付费版命令行应用
71 |
72 | - `npm run start:pro-cli`(需求提前安装docker)
73 |
74 | ## 捐赠
75 |
76 | 程序开发不易,需要大量的时间和精力。如果有人为开发捐款,我将非常感谢。
77 |
78 | ### BTC
79 |
80 | `1J3hX6en3147VtEJvS2WbFrJ1emNcfcTdz`
81 |
82 | ### ETH
83 |
84 | `0x8bb4a5f034B4822E0D1B51f4E07ce1eee7Bc8D8C`
85 |
86 |
87 | ## License: GPL3
88 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # triangular-arbitrage2
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | an open source automated triangular arbitrage trading program。
10 | The previous version of the program: https://github.com/zlq4863947/triangular-arbitrage
11 |
12 | 查看中文
13 |
14 | ## Technology Architecture
15 |
16 | - Development language: [typescript](https://github.com/microsoft/TypeScript)
17 | - Technical library: [rxjs](https://github.com/ReactiveX/rxjs) 、[nestjs](https://github.com/nestjs/nest)
18 | - Cryptocurrency related library: [ccxt](https://github.com/ccxt/ccxt) 、[binance](https://github.com/tiagosiebler/binance)
19 | - Test library: [jest](https://github.com/facebook/jest)
20 | - Virtual environment container: docker
21 | - database: mysql 8.0
22 | - Dependency management tools: [yarn](https://github.com/yarnpkg/yarn) (Not npm)
23 |
24 | ## Disclaimer
25 |
26 | - `triangular-arbitrage2` is NOT a sure-fire profit machine. Use it AT YOUR OWN RISK.
27 | - Cryptocurrency is still an experiment, and therefore so is `triangular-arbitrage2`. Meaning, both may fail at any time.
28 | - `triangular-arbitrage2`Divided into `Basic` (free version) and `Pro` (Paid version).
29 | - `Basic`(Free version), there is no real trading function, you need to add this function yourself, and bear the risks arising from it.
30 | - `Pro`(Paid version), I am responsible for ensuring and maintaining the accuracy of this robot's strategy and transactions. The percentage profit is not guaranteed, and the risks arising from the transaction shall be borne by myself.
31 | - Never leave the bot un-monitored for long periods of time. `triangular-arbitrage2` doesn't know when to stop, so be prepared to stop it if too much loss occurs.
32 |
33 | ## Robot description
34 |
35 | | | Basic(Free version) | Pro(Paid version) |
36 | |--|--|--|
37 | | Open source | ○ | △ |
38 | | Command line application | ○ | ○ |
39 | | Supported exchanges | 1(binance) | 1+n (binance+Other exchanges) |
40 | | Multiple types of logging | ○ | ○ |
41 | | Automatic calculation of transaction fees | ○ | ○ |
42 | | Simulated transaction | ○ | ○ |
43 | | Real transaction | × | ○ |
44 | | Income statement| × | ○ |
45 | | Multiple execution strategies | × | ○ |
46 | | Maintenance and support | github issue | real time |
47 |
48 | - For the paid version, you can contact us for consultation through the following methods
49 | - email: zlq4863947@gmail.com
50 | - qq: 442540141
51 |
52 | ## Quick start
53 |
54 | ### 1,Environmental installation
55 |
56 | - Install nodejs (the latest version is fine)
57 | - Execute `npm install` (depends on the installer)
58 |
59 | ### 2、Setup configuration file
60 |
61 | - **config/default.sample.toml**change to**config/default.toml**
62 | - Modify it yourself to the desired configuration,ig:Binance's apikey
63 |
64 | ### 3、Start the command line bot
65 |
66 | #### Free version of the command line application
67 |
68 | - `npm run start:cli`
69 |
70 | #### Paid version of the command line application
71 |
72 | - `npm run start:pro-cli`(Need to install docker in advance)
73 |
74 |
75 | ## Donate
76 |
77 | Program development is not easy and requires a lot of time and energy. If anyone contributes to the development, I would be very grateful.
78 |
79 | ### BTC
80 |
81 | `1J3hX6en3147VtEJvS2WbFrJ1emNcfcTdz`
82 |
83 | ### ETH
84 |
85 | `0x8bb4a5f034B4822E0D1B51f4E07ce1eee7Bc8D8C`
86 |
87 | ## License: GPL3
88 |
--------------------------------------------------------------------------------
/apps/cli-app/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { BrokerApiModule } from '@ta2-libs/broker-api';
3 | import { LogLevels, LoggerModule, MultiLogStrategy } from '@ta2-libs/logger';
4 | import { DataModule, EngineModule, SharedModule, TradeModule } from '@ta2-libs/modules';
5 |
6 | import { AppService } from './app.service';
7 | import { StrategyModule } from './strategy';
8 |
9 | @Module({
10 | imports: [
11 | BrokerApiModule,
12 | DataModule,
13 | StrategyModule,
14 | EngineModule,
15 | TradeModule,
16 | LoggerModule.forRoot({
17 | enableColors: true,
18 | minLogLevel: LogLevels.Debug,
19 | strategy: MultiLogStrategy,
20 | }),
21 | SharedModule,
22 | ],
23 | providers: [AppService],
24 | })
25 | export class AppModule {}
26 |
--------------------------------------------------------------------------------
/apps/cli-app/src/app/app.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { Logger } from '@ta2-libs/logger';
3 |
4 | import { AppService } from './app.service';
5 |
6 | describe('AppService', () => {
7 | let service: AppService;
8 |
9 | beforeEach(async () => {
10 | const module: TestingModule = await Test.createTestingModule({
11 | providers: [Logger, AppService],
12 | }).compile();
13 |
14 | service = module.get(AppService);
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/apps/cli-app/src/app/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { CatchError, floorToString } from '@ta2-libs/common';
3 | import { DefaultExceptionHandler } from '@ta2-libs/exceptions';
4 | import { Logger } from '@ta2-libs/logger';
5 | import { Triangle } from '@ta2-libs/models';
6 | import { DataService, EngineService, OnDestroyService, TradeService } from '@ta2-libs/modules';
7 |
8 | import moment = require('moment');
9 | // eslint-disable-next-line @typescript-eslint/no-var-requires
10 | const AsciiTable = require('ascii-table');
11 |
12 | @Injectable()
13 | @CatchError(DefaultExceptionHandler)
14 | export class AppService extends OnDestroyService {
15 | private name = 'AppService';
16 |
17 | constructor(
18 | private logger: Logger,
19 | private dataService: DataService,
20 | private engineService: EngineService,
21 | private tradeService: TradeService,
22 | ) {
23 | super();
24 | }
25 |
26 | start(printTable?: boolean): void {
27 | this.logger.info(this.name, `启动三角套利机器人...`);
28 | this.engineService.getTradableTriangle$().subscribe((triangle) => this.tradeService.start(triangle, this.dataService.tickers));
29 | /*if (printTable) {
30 | this.engineService.getCandidates$().subscribe(this.printTable);
31 | }*/
32 | }
33 |
34 | printTable(triangles: Triangle[]): void {
35 | const table = new AsciiTable('三角套利候选列表');
36 | table.setHeading('', '路径', '利率', '产生时间');
37 | for (const [index, triangle] of triangles.entries()) {
38 | table.addRow(
39 | index + 1,
40 | triangle.id,
41 | `${floorToString(triangle.rate, 4)}%`,
42 | moment(triangle.time).format('YYYY-MM-DDTHH:mm:ss.SSSZZ'),
43 | );
44 | }
45 | console.log(table.toString());
46 | table.clear();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/apps/cli-app/src/app/index.ts:
--------------------------------------------------------------------------------
1 | export * from './app.module';
2 | export * from './app.service';
3 |
--------------------------------------------------------------------------------
/apps/cli-app/src/app/strategy/index.ts:
--------------------------------------------------------------------------------
1 | export * from './strategy.module';
2 | export * from './strategy.service';
3 |
--------------------------------------------------------------------------------
/apps/cli-app/src/app/strategy/strategy.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { Strategy, TRADING_STRATEGY } from '@ta2-libs/models';
3 |
4 | import { StrategyService } from './strategy.service';
5 |
6 | @Global()
7 | @Module({
8 | providers: [
9 | {
10 | provide: TRADING_STRATEGY as any,
11 | useClass: StrategyService,
12 | },
13 | Strategy,
14 | ],
15 | exports: [Strategy],
16 | })
17 | export class StrategyModule {}
18 |
--------------------------------------------------------------------------------
/apps/cli-app/src/app/strategy/strategy.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { CatchError } from '@ta2-libs/common';
3 | import { DefaultExceptionHandler } from '@ta2-libs/exceptions';
4 | import { Logger } from '@ta2-libs/logger';
5 | import { TradeStatus, TradeTriangle, TradingStrategy } from '@ta2-libs/models';
6 | import { EMPTY, Observable } from 'rxjs';
7 |
8 | @Injectable()
9 | @CatchError(DefaultExceptionHandler)
10 | export class StrategyService implements TradingStrategy {
11 | private name = 'StrategyService';
12 | private triangle: TradeTriangle | undefined = undefined;
13 | constructor(private logger: Logger) {}
14 |
15 | async execute(triangle: TradeTriangle): Promise {
16 | triangle.status = TradeStatus.Open;
17 | triangle.openTime = Date.now();
18 | this.triangle = triangle;
19 | this.logger.info(this.name, `套利交易信息:${JSON.stringify(triangle)}`);
20 | }
21 |
22 | getResult$(): Observable {
23 | return EMPTY;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/apps/cli-app/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { Logger } from '@ta2-libs/logger';
3 | import { NotificationManager } from '@ta2-libs/notifications';
4 |
5 | import { AppModule, AppService } from './app';
6 |
7 | declare global {
8 | // eslint-disable-next-line @typescript-eslint/no-namespace
9 | namespace NodeJS {
10 | interface Global {
11 | logger: Logger;
12 | notification: NotificationManager;
13 | pro: boolean;
14 | }
15 | }
16 | }
17 |
18 | async function bootstrap() {
19 | try {
20 | const app = await NestFactory.createApplicationContext(AppModule);
21 | const logger = app.get(Logger);
22 | app.useLogger(logger);
23 | global.logger = logger;
24 | global.notification = new NotificationManager();
25 | global.pro = false;
26 |
27 | const appService = app.get(AppService);
28 | appService.start(true);
29 | } catch (e) {
30 | printError('bootstrapException', e);
31 | }
32 | }
33 | bootstrap();
34 |
35 | process.on('uncaughtException', async (err) => printError('uncaughtException', err));
36 |
37 | function printError(label: string, error: Error) {
38 | const logger = global.logger ? global.logger : console;
39 | logger.error(label, error);
40 | }
41 |
--------------------------------------------------------------------------------
/apps/cli-app/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": false,
5 | "outDir": "../../dist/apps/cli-app"
6 | },
7 | "include": ["src/**/*"],
8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
37 |
--------------------------------------------------------------------------------
/assets/images/logo_circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zlq4863947/triangular-arbitrage2/b26b2c81bd662e1d70e62e37609b6d1c4be9423a/assets/images/logo_circle.png
--------------------------------------------------------------------------------
/assets/images/logo_circle.svg:
--------------------------------------------------------------------------------
1 |
39 |
--------------------------------------------------------------------------------
/config/default.sample.toml:
--------------------------------------------------------------------------------
1 | # 配置文件
2 |
3 | # 套利交易所
4 | active = "binance"
5 | # 下单倍数(最小下单数量的倍数)
6 | orderTimes = 2
7 | # 未成交订单巡查时间(一次性、单位: 秒)
8 | processingOrderPatrolTime = 60
9 | # 最大同时进行的交易会话数
10 | sessionLimit = 1
11 |
12 | ###
13 | ### [mysql]
14 | ### mysql
15 | ###
16 | [mysql]
17 | host = "127.0.0.1"
18 | port = 13306
19 | username = "root"
20 | password = "qwer1234"
21 | database = "ta_db"
22 | logging = true
23 |
24 | ###
25 | ### [notification]
26 | ### 消息通知
27 | ###
28 | [notification]
29 | # email
30 | [notification.email]
31 | enabled = false
32 | # 查看支持列表:https://nodemailer.com/smtp/well-known/
33 | smtpService = "gmail"
34 | # 邮箱3账号
35 | authUser = "test@gmail.com"
36 | # smtp授权码
37 | authPass = "smtp password"
38 | # 发送邮件列表
39 | sendList = ["test@gmail.com"]
40 |
41 | ###
42 | ### [broker]
43 | ### 交易所相关配置
44 | ###
45 | [broker]
46 | # binance
47 | [broker.binance]
48 | # 套利收益率(利率需覆盖手续费,才可以盈利)。 单位为百分比,例如配置为0.1,实际含义:0.1%
49 | profitRate = 0.1
50 | # 起始货币数组(全市场都可作为起始币时:[]) 'BTC', 'ETH', 'USDT', 'BNB'
51 | startAssets = ['BTC', 'ETH', 'USDT', 'BNB']
52 | # 白名单(设置后C点货币,只会出现白名单列表中的货币)
53 | # 白名单和黑名单同时设置时, 以白名单为准
54 | whitelist = []
55 | # 黑名单(设置后C点货币,不会出现黑名单列表中的货币)
56 | blacklist = ['ONT', 'MCO']
57 | # 运行模式
58 | # test : 模拟交易
59 | # real : 真实交易
60 | mode = "real"
61 | [broker.binance.real]
62 | apiKey = "zcc"
63 | secret = "wer"
64 | [broker.binance.test]
65 | apiKey = "xx"
66 | secret = "aaa"
67 |
68 | ###
69 | ### [pro]
70 | ### 付费版专用功能
71 | ###
72 | [pro]
73 | # 三角套利执行策略
74 | # 目前支持的值: 'queue'、'multi'
75 | # queue: 将按照三边顺序执行下单(上一边未成交完成,将阻塞下一边),适用于三边交易对没有余额的情况
76 | # multi: 将按照三边同时执行下单(上一边未成交完成,不阻塞下一边),适用于三边交易对都有余额的情况
77 | strategy = 'queue'
78 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | mysql:
5 | image: 'mysql:8.0.26'
6 | container_name: 'mysql'
7 | expose:
8 | - '3306'
9 | ports:
10 | - '13306:3306'
11 | environment:
12 | MYSQL_ROOT_PASSWORD: 'qwer1234'
13 | MYSQL_USER: 'test'
14 | MYSQL_PASSWORD: 'qwer1234'
15 | MYSQL_DATABASE: 'ta_db'
16 | command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci --sql-mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
17 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /**
3 | * Load the TypeScript compiler, then load the TypeScript gulpfile which simply loads all
4 | * the tasks. The tasks are really inside tools/gulp/tasks.
5 | */
6 | const path = require('path');
7 |
8 | const tsconfigPath = path.join(__dirname, './tools/gulp/tsconfig.json');
9 |
10 | // Register TS compilation.
11 | require('ts-node').register({
12 | project: tsconfigPath,
13 | });
14 |
15 | require('./tools/gulp/gulpfile');
16 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | setupFilesAfterEnv: ['./jest.setup.js'],
4 | moduleFileExtensions: ['js', 'json', 'ts'],
5 | moduleNameMapper: {
6 | '^apps/(.*)$': '/apps/$1',
7 | '^@ta2-libs/(.*)': '/libs/$1',
8 | },
9 | modulePathIgnorePatterns: ['/dist'],
10 | testMatch: ['/(apps|libs)/**/*.spec.ts'],
11 | transform: {
12 | '^.+\\.ts$': 'ts-jest',
13 | },
14 | collectCoverageFrom: ['src/**/*.{js,jsx,tsx,ts}', '!**/node_modules/**', '!**/vendor/**'],
15 | coverageReporters: ['json', 'lcov'],
16 | verbose: true,
17 | forceExit: true,
18 | testEnvironment: 'node',
19 | };
20 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | jest.setTimeout(50000);
2 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/binance-api.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 |
3 | import { BinanceApiService, BinanceRestClient, BinanceWebsocketClient, WebsocketHandler } from './services';
4 |
5 | @Module({
6 | exports: [BinanceApiService, BinanceWebsocketClient, WebsocketHandler, BinanceRestClient],
7 | providers: [BinanceApiService, WebsocketHandler, BinanceWebsocketClient, BinanceRestClient],
8 | })
9 | export class BinanceApiModule {}
10 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/constants.ts:
--------------------------------------------------------------------------------
1 | export const brokerConfig = {
2 | // 所有wss接口baseurl https://binance-docs.github.io/apidocs/futures/cn/#669b9d47c2
3 | websocketBaseUrl: 'wss://stream.binance.com:9443/ws/',
4 | websocketCombinedBaseUrl: 'wss://stream.binance.com:9443/stream?streams=',
5 | };
6 |
7 | export const WsMarketEndpoints = {
8 | AllTickers: 'allTickers',
9 | } as const;
10 |
11 | export type WsMarketEndpoints = typeof WsMarketEndpoints[keyof typeof WsMarketEndpoints];
12 |
13 | export const WsUserEndpoints = {
14 | UserData: 'userData',
15 | } as const;
16 |
17 | export type WsUserEndpoints = typeof WsUserEndpoints[keyof typeof WsUserEndpoints];
18 |
19 | export const WsEndpoints = {
20 | ...WsMarketEndpoints,
21 | ...WsUserEndpoints,
22 | } as const;
23 |
24 | export type WsEndpoints = WsUserEndpoints | WsMarketEndpoints;
25 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './binance-api.module';
2 | export * from './services';
3 | export * from './types';
4 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/binance-api/binance-api.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { BinanceApiModule, BinanceApiService } from '@ta2-libs/broker-api';
3 | import { MockModule } from '@ta2-libs/testing';
4 |
5 | describe('BinanceApiService', () => {
6 | let service: BinanceApiService;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | imports: [MockModule, BinanceApiModule],
11 | }).compile();
12 |
13 | service = module.get(BinanceApiService);
14 | await service.onModuleInit();
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/binance-api/binance-api.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, OnModuleInit } from '@nestjs/common';
2 | import { BehaviorSubject } from 'rxjs';
3 |
4 | import { BinanceRestClient } from '../binance-rest-client/binance-rest-client.service';
5 | import { BinanceWebsocketClient } from '../binance-websocket-client/binance-websocket-client.service';
6 |
7 | @Injectable()
8 | export class BinanceApiService implements OnModuleInit {
9 | onReady = new BehaviorSubject(false);
10 | constructor(public ws: BinanceWebsocketClient, public rest: BinanceRestClient) {}
11 |
12 | async onModuleInit(): Promise {
13 | await this.rest.initialize();
14 | this.ws.initialize(this.rest.getListenKeyRest());
15 | this.onReady.next(true);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/binance-rest-client/binance-rest-client.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 |
3 | import { BinanceRestClient } from './binance-rest-client.service';
4 |
5 | describe('BinanceRestClient', () => {
6 | let service: BinanceRestClient;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | providers: [BinanceRestClient],
11 | }).compile();
12 |
13 | service = module.get(BinanceRestClient);
14 | await service.initialize();
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 |
21 | it('fetchTradingFees', async () => {
22 | const res = await service.fetchTradingFees();
23 | const asset = res['ETH/BUSD'];
24 | expect(asset).toBeDefined();
25 | expect(Object.keys(res).length).toBeGreaterThan(0);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/binance-rest-client/binance-rest-client.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { CatchError } from '@ta2-libs/common';
3 | import { Config } from '@ta2-libs/config';
4 | import { DefaultExceptionHandler } from '@ta2-libs/exceptions';
5 | import * as ccxt from 'ccxt';
6 | import { Balances, Market, Order } from 'ccxt';
7 |
8 | import { AssetMarkets, OrderParams, PairFees, Pairs } from '../../types';
9 |
10 | // eslint-disable-next-line @typescript-eslint/no-var-requires
11 | const binance = require('binance');
12 |
13 | @Injectable()
14 | @CatchError(DefaultExceptionHandler)
15 | export class BinanceRestClient {
16 | public pairs: Pairs;
17 | public pairFees: PairFees;
18 | public assetMarkets: AssetMarkets;
19 | private _listenKeyRest: any;
20 | private ccxt: ccxt.binance;
21 |
22 | async initialize(): Promise {
23 | this._listenKeyRest = new binance.BinanceRest({
24 | key: Config.credential.apiKey,
25 | secret: Config.credential.secret,
26 | timeout: 15000,
27 | recvWindow: 10000,
28 | disableBeautification: false,
29 | handleDrift: false,
30 | });
31 | this.ccxt = new ccxt.binance(Config.credential);
32 | this.pairs = await this.getPairs();
33 | this.pairFees = await this.fetchTradingFees();
34 | await this.initAssetMarkets(Object.keys(this.pairs));
35 | }
36 |
37 | getListenKeyRest(): any {
38 | return this._listenKeyRest;
39 | }
40 |
41 | getBalance(): Promise {
42 | return this.ccxt.fetchBalance();
43 | }
44 |
45 | getPairInfo(pairName: string): Market | undefined {
46 | return this.pairs[pairName];
47 | }
48 |
49 | fetchOrder(orderId: string, symbol: string): Promise {
50 | return this.ccxt.fetchOrder(orderId, symbol);
51 | }
52 |
53 | createOrder(params: OrderParams): Promise {
54 | return this.ccxt.createOrder(params.symbol, params.type, params.side, params.amount, params.price, {
55 | newClientOrderId: params.newClientOrderId,
56 | });
57 | }
58 |
59 | editOrder(id: string, params: OrderParams): Promise {
60 | return this.ccxt.editOrder(id, params.symbol, params.type, params.side, params.amount, params.price, {
61 | newClientOrderId: params.newClientOrderId,
62 | });
63 | }
64 |
65 | fetchTradingFees(): Promise {
66 | return this.ccxt.fetchTradingFees();
67 | }
68 |
69 | private async getPairs(): Promise {
70 | return this.ccxt.loadMarkets();
71 | }
72 |
73 | private initAssetMarkets(pairNames: string[]): void {
74 | this.assetMarkets = {};
75 | for (const pairName of pairNames) {
76 | const assetName = pairName.substr(pairName.indexOf('/') + 1);
77 | if (assetName) {
78 | const market = this.pairs[pairName];
79 | if (!this.assetMarkets[assetName]) {
80 | this.assetMarkets[assetName] = [market];
81 | } else {
82 | this.assetMarkets[assetName].push(market);
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/binance-websocket-client/binance-websocket-client.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { Config } from '@ta2-libs/config';
3 | import { MockModule } from '@ta2-libs/testing';
4 |
5 | import { WebsocketHandler } from '../websocket-handler/websocket-handler.service';
6 | import { BinanceWebsocketClient } from './binance-websocket-client.service';
7 |
8 | // eslint-disable-next-line @typescript-eslint/no-var-requires
9 | const binance = require('binance');
10 |
11 | describe('BinanceWebsocketClientService', () => {
12 | let service: BinanceWebsocketClient;
13 |
14 | beforeEach(async () => {
15 | const module: TestingModule = await Test.createTestingModule({
16 | imports: [MockModule],
17 | providers: [WebsocketHandler, BinanceWebsocketClient],
18 | }).compile();
19 |
20 | service = module.get(BinanceWebsocketClient);
21 | service.initialize({} as any);
22 | });
23 |
24 | it('should be defined', () => {
25 | expect(service).toBeDefined();
26 | });
27 |
28 | it('should get getAllTickers$', (done) => {
29 | service.getAllTickers$().subscribe((data) => {
30 | expect(Object.keys(data).length > 0).toBeTruthy();
31 | done();
32 | });
33 | });
34 |
35 | it('should get getUserData$', (done) => {
36 | const rest = new binance.BinanceRest({
37 | key: Config.credential.apiKey,
38 | secret: Config.credential.secret,
39 | timeout: 15000,
40 | recvWindow: 10000,
41 | disableBeautification: false,
42 | handleDrift: false,
43 | });
44 |
45 | service.getUserData$(rest).subscribe((data) => {
46 | expect(data).toBeDefined();
47 | done();
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/binance-websocket-client/binance-websocket-client.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { CatchError } from '@ta2-libs/common';
3 | import { DefaultExceptionHandler } from '@ta2-libs/exceptions';
4 | import { Observable } from 'rxjs';
5 | import { map } from 'rxjs/operators';
6 |
7 | import { WsEndpoints, WsMarketEndpoints } from '../../constants';
8 | import { Ticker24Hr, Tickers, UserData } from '../../types';
9 | import { WebsocketHandler } from '../websocket-handler/websocket-handler.service';
10 |
11 | @Injectable()
12 | @CatchError(DefaultExceptionHandler)
13 | export class BinanceWebsocketClient {
14 | private rest: any;
15 | constructor(private websocketHandler: WebsocketHandler) {}
16 |
17 | initialize(rest: any): void {
18 | this.rest = rest;
19 | this.websocketHandler.initialize();
20 | }
21 |
22 | getAllTickers$(): Observable {
23 | return this.websocketHandler.subscribe(WsMarketEndpoints.AllTickers).pipe(
24 | map((oTickers) => {
25 | const tickers: Tickers = {};
26 | oTickers.forEach((o) => (tickers[o.symbol] = o));
27 | return tickers;
28 | }),
29 | );
30 | }
31 |
32 | disposeAllTickers(): void {
33 | return this.websocketHandler.unsubscribe(WsMarketEndpoints.AllTickers);
34 | }
35 |
36 | getUserData$(rest?: any): Observable {
37 | return this.websocketHandler.subscribeUserData(WsEndpoints.UserData, rest ? rest : this.rest);
38 | }
39 |
40 | disposeExecutionReport(): void {
41 | return this.websocketHandler.unsubscribe(WsEndpoints.UserData);
42 | }
43 |
44 | disposeAllSubscriptions(): void {
45 | return this.websocketHandler.unsubscribeAll();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './binance-rest-client/binance-rest-client.service';
2 | export * from './binance-websocket-client/binance-websocket-client.service';
3 | export * from './websocket-handler/websocket-handler.service';
4 | export * from './binance-api/binance-api.service';
5 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/websocket-handler/websocket-handler.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 |
3 | import { WebsocketHandler } from './websocket-handler.service';
4 |
5 | describe('WebsocketHandlerService', () => {
6 | let service: WebsocketHandler;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | providers: [WebsocketHandler],
11 | }).compile();
12 |
13 | service = module.get(WebsocketHandler);
14 | service.initialize();
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(service).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/services/websocket-handler/websocket-handler.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, OnModuleDestroy } from '@nestjs/common';
2 | import { Logger } from '@ta2-libs/logger';
3 | import { BehaviorSubject, Observable, Subject } from 'rxjs';
4 | import { distinctUntilChanged } from 'rxjs/operators';
5 | import * as WebSocket from 'ws';
6 |
7 | import { WsEndpoints, WsMarketEndpoints, WsUserEndpoints } from '../../constants';
8 |
9 | // eslint-disable-next-line @typescript-eslint/no-var-requires
10 | const binance = require('binance');
11 |
12 | interface StreamData {
13 | subject: Subject;
14 | websocket: WebSocket;
15 | }
16 | @Injectable()
17 | export class WebsocketHandler implements OnModuleDestroy {
18 | private name = 'WebsocketHandler';
19 | private onDestroy$ = new Subject();
20 |
21 | private wsWrapper: any;
22 | private restWrapper: any;
23 | private streamMap = new Map>();
24 | private joinedEndpointSet = new Set();
25 |
26 | /**
27 | * 当前是否已连接websocket。
28 | * 值将通过 this.notifyConnectionStatus() 方法发出。
29 | */
30 | private _isConnected$ = new BehaviorSubject(false);
31 |
32 | /**
33 | * 指示是否可以重新连接到服务器。
34 | * 当通过调用断开连接方法手动关闭连接时,该值将设置为false。
35 | *
36 | * @type {boolean}
37 | */
38 | private allowReconnect = true;
39 |
40 | get isConnected$(): Observable {
41 | return this._isConnected$.asObservable().pipe(distinctUntilChanged());
42 | }
43 |
44 | get isConnected(): boolean {
45 | return false; //this.socket.connected;
46 | }
47 |
48 | constructor(private logger: Logger) {}
49 |
50 | initialize(): void {
51 | this.wsWrapper = new binance.BinanceWS();
52 | }
53 |
54 | subscribe(endpoint: WsMarketEndpoints): Observable {
55 | return this.getSubjectByStream(endpoint).asObservable();
56 | }
57 |
58 | subscribeUserData(endpoint: WsUserEndpoints, listenKeyRest: any): Observable {
59 | this.restWrapper = listenKeyRest;
60 |
61 | return this.getSubjectByStream(endpoint).asObservable();
62 | }
63 |
64 | unsubscribe(endpoint: WsEndpoints): void {
65 | const stream = this.streamMap.get(endpoint);
66 | if (!stream) {
67 | return;
68 | }
69 | this.logger.debug(this.name, 'unsubscribe stream', endpoint);
70 | stream.subject.complete();
71 | stream.websocket.close();
72 | this.streamMap.delete(endpoint);
73 | this.joinedEndpointSet.delete(endpoint);
74 | }
75 |
76 | unsubscribeAll(): void {
77 | this.logger.debug(this.name, 'unsubscribe all stream');
78 | for (const endpoint of Array.from(this.streamMap.keys())) {
79 | this.unsubscribe(endpoint);
80 | }
81 | }
82 |
83 | onModuleDestroy(): void {
84 | this.unsubscribeAll();
85 | this.onDestroy$.next(true);
86 | this.onDestroy$.complete();
87 | }
88 |
89 | private getSubjectByStream(endpoint: WsEndpoints): Subject {
90 | const stream = this.streamMap.get(endpoint);
91 | if (stream) {
92 | return stream.subject as Subject;
93 | }
94 |
95 | const subject = new Subject();
96 | const websocket = this.joinStream(endpoint, subject);
97 | this.streamMap.set(endpoint, { subject, websocket });
98 |
99 | return subject;
100 | }
101 |
102 | private joinStream(endpoint: WsEndpoints, subject: Subject): WebSocket {
103 | if (!this.joinedEndpointSet.has(endpoint)) {
104 | this.joinedEndpointSet.add(endpoint);
105 | }
106 | switch (endpoint) {
107 | case WsEndpoints.AllTickers: {
108 | return this.wsWrapper.onAllTickers((data) => subject.next(data));
109 | }
110 | case WsEndpoints.UserData: {
111 | if (!this.restWrapper) {
112 | this.logger.error(this.name, 'restWrapper is null', endpoint);
113 | return;
114 | }
115 |
116 | return this.wsWrapper.onUserData(this.restWrapper, (data) => subject.next(data));
117 | }
118 | }
119 | }
120 |
121 | private reJoinStreams(): void {
122 | this.joinedEndpointSet.forEach((endpoint) => {
123 | const stream = this.streamMap.get(endpoint);
124 | stream.websocket = this.joinStream(endpoint, stream.subject);
125 | });
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ws';
2 | export * from './pair';
3 | export * from './order';
4 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/types/order.ts:
--------------------------------------------------------------------------------
1 | export interface OrderParams {
2 | symbol: string; // symbol in CCXT format
3 | amount: number; // amount of base currency
4 | price: number; // float price in quote currency
5 | type: 'market' | 'limit'; // order type, 'market', 'limit' or undefined/None/null
6 | side: 'buy' | 'sell';
7 | newClientOrderId?: string;
8 | }
9 |
10 | export interface TradeFee {
11 | symbol: string;
12 | maker: number;
13 | taker: number;
14 | }
15 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/types/pair.ts:
--------------------------------------------------------------------------------
1 | import { Market } from 'ccxt';
2 |
3 | import { TradeFee } from './order';
4 |
5 | export interface Pairs {
6 | [pair: string]: Market;
7 | }
8 |
9 | export interface AssetMarkets {
10 | [asset: string]: Market[];
11 | }
12 |
13 | export interface AssetMarket {
14 | [asset: string]: Market;
15 | }
16 |
17 | export interface PairFees {
18 | [pair: string]: TradeFee;
19 | }
20 |
--------------------------------------------------------------------------------
/libs/broker-api/binance-api/types/ws.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 事件类型
3 | */
4 | export const EventType = {
5 | // 24小时Ticker
6 | Ticker24hr: '24hrTicker',
7 | // 订单更新
8 | ExecutionReport: 'executionReport',
9 | // 帐户余额发生变化
10 | OutboundAccountPosition: 'outboundAccountPosition',
11 | } as const;
12 |
13 | export type EventType = typeof EventType[keyof typeof EventType];
14 |
15 | export interface EventData {
16 | eventType: EventType; // "24hrTicker" or "executionReport", 事件类型
17 | eventTime: number; // 123456789, 事件时间
18 | symbol: string; // "BNBBTC", 交易对
19 | }
20 |
21 | export interface Ticker24Hr extends EventData {
22 | priceChange: string; // "0.0015", 24小时价格变化
23 | priceChangePercent: string; // "250.00", 24小时价格变化(百分比)
24 | weightedAveragePrice: string; // "0.0018", 平均价格
25 | previousClose: string; // "0.0009", 整整24小时之前,向前数的最后一次成交价格
26 | currentClose: string; // "0.0025", 最新成交价格
27 | closeQuantity: string; // "10", 最新成交交易的成交量
28 | bestBid: string; // "0.0024", 目前最高买单价
29 | bestBidQuantity: string; // "10", 目前最高买单价的挂单量
30 | bestAskPrice: string; // "0.0026", 目前最低卖单价
31 | bestAskQuantity: string; // "100", 目前最低卖单价的挂单量
32 | open: string; // "0.0010", 整整24小时前,向后数的第一次成交价格
33 | high: string; // "0.0025", 24小时内最高成交价
34 | low: string; // "0.0010", 24小时内最低成交价
35 | baseAssetVolume: string; // "10000", 24小时内成交量
36 | quoteAssetVolume: string; // "18", 24小时内成交额
37 | openTime: number; // 0, 统计开始时间
38 | closeTime: number; // 86400000, 统计结束时间
39 | firstTradeId: number; // 0, 24小时内第一笔成交交易ID
40 | lastTradeId: number; // 18150, 24小时内最后一笔成交交易ID
41 | trades: number; // 18151 24小时内成交数
42 | }
43 |
44 | /**
45 | * 执行类型
46 | */
47 | export const ExecutionType = {
48 | // 新订单
49 | NEW: 'NEW',
50 | // 订单被取消
51 | CANCELED: 'CANCELED',
52 | // 新订单被拒绝
53 | REJECTED: 'REJECTED',
54 | // 订单有新成交
55 | TRADE: 'TRADE',
56 | // 订单失效(根据订单的Time In Force参数)
57 | EXPIRED: 'EXPIRED',
58 | FILLED: 'FILLED',
59 | } as const;
60 |
61 | export type ExecutionType = typeof ExecutionType[keyof typeof ExecutionType];
62 |
63 | export interface UserBalance {
64 | asset: string;
65 | availableBalance: string;
66 | onOrderBalance: string;
67 | }
68 |
69 | /**
70 | * 订单更新
71 | * https://binance-docs.github.io/apidocs/spot/cn/#payload-2
72 | */
73 | export interface UserData extends EventData {
74 | newClientOrderId: string; // "IoyjBHK8OIGkFJN2DtCX6N", Client order ID
75 | side: string; // "SELL", 订单方向
76 | orderType: string; // "LIMIT", 订单类型
77 | cancelType: string; // "GTC", 有效方式
78 | quantity: string; // "0.02700000", 订单原始数量
79 | price: string; // "0.07209100", 订单原始价格
80 | stopPrice: string; // "0.00000000", 止盈止损单触发价格
81 | icebergQuantity: string; // "0.00000000", 冰山订单数量
82 | originalClientOrderId: string; // "null", 原始订单自定义ID(原始订单,指撤单操作的对象。撤单本身被视为另一个订单)
83 | executionType: ExecutionType; // "NEW", 本次事件的具体执行类型
84 | orderStatus: ExecutionType; // "NEW", 订单的当前状态
85 | rejectReason: string; // "NONE", 订单被拒绝的原因
86 | orderId: number; // 174251668, Order ID
87 | lastTradeQuantity: string; // "0.00000000", 订单末次成交量
88 | accumulatedQuantity: string; // "0.00000000", 订单累计已成交量
89 | lastTradePrice: string; // "0.00000000", 订单末次成交价格
90 | commission: string; // "0", 手续费数量
91 | commissionAsset: any; // null, 手续费资产类别
92 | tradeTime: number; // 1530435177265, 成交时间
93 | tradeId: number; // 18151 成交ID
94 | maker: boolean; // false maker side
95 | balances: UserBalance[];
96 | }
97 |
98 | export interface Tickers {
99 | [pair: string]: Ticker24Hr;
100 | }
101 |
--------------------------------------------------------------------------------
/libs/broker-api/broker-api.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 |
3 | import { BinanceApiModule } from './binance-api';
4 |
5 | @Global()
6 | @Module({
7 | imports: [BinanceApiModule],
8 | exports: [BinanceApiModule],
9 | })
10 | export class BrokerApiModule {}
11 |
--------------------------------------------------------------------------------
/libs/broker-api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './broker-api.module';
2 | export * from './binance-api';
3 |
--------------------------------------------------------------------------------
/libs/common/big-number/calculations.spec.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 |
3 | import { addToString, divideToString, multipleToString, oppositeToString, subtractToString } from './calculations';
4 |
5 | describe('BigNumber util functions', () => {
6 | describe('addToString', () => {
7 | it('should return expected value.', () => {
8 | expect(addToString(1, 2)).toEqual('3');
9 | expect(addToString(1, 2, 3)).toEqual('6');
10 | expect(addToString(1, 2, 3, 4)).toEqual('10');
11 | expect(addToString('1', '2')).toEqual('3');
12 | expect(addToString(new BigNumber(1), new BigNumber(2))).toEqual('3');
13 | });
14 |
15 | it('should throw if invalid value', () => {
16 | expect(() => addToString(null as any, null as any)).toThrow();
17 | expect(() => addToString(undefined as any, undefined as any)).toThrow();
18 | expect(() => addToString('', '')).toThrow();
19 | });
20 | });
21 |
22 | describe('subtractToString', () => {
23 | it('should return expected value.', () => {
24 | expect(subtractToString(1, 2)).toEqual('-1');
25 | expect(subtractToString(1, 2, 3)).toEqual('-4');
26 | expect(subtractToString(1, 2, 3, 4)).toEqual('-8');
27 | expect(subtractToString('1', '2')).toEqual('-1');
28 | expect(subtractToString(new BigNumber(1), new BigNumber(2))).toEqual('-1');
29 | });
30 |
31 | it('should throw if invalid value', () => {
32 | expect(() => subtractToString(null as any, null as any)).toThrow();
33 | expect(() => subtractToString(undefined as any, undefined as any)).toThrow();
34 | expect(() => subtractToString('', '')).toThrow();
35 | });
36 | });
37 |
38 | describe('multipleToString', () => {
39 | it('should return expected value.', () => {
40 | expect(multipleToString(1, 2)).toEqual('2');
41 | expect(multipleToString(1, 2, 3)).toEqual('6');
42 | expect(multipleToString(1, 2, 3, 4)).toEqual('24');
43 | expect(multipleToString('1', '2')).toEqual('2');
44 | expect(multipleToString(new BigNumber(1), new BigNumber(2))).toEqual('2');
45 | });
46 |
47 | it('should throw if invalid value', () => {
48 | expect(() => multipleToString(null as any, null as any)).toThrow();
49 | expect(() => multipleToString(undefined as any, undefined as any)).toThrow();
50 | expect(() => multipleToString('', '')).toThrow();
51 | });
52 | });
53 |
54 | describe('divideToString', () => {
55 | it('should return expected value.', () => {
56 | expect(divideToString(1, 2)).toEqual('0.5');
57 | expect(divideToString(1, 2, 2)).toEqual('0.25');
58 | expect(divideToString(1, 2, 2, 2)).toEqual('0.125');
59 | expect(divideToString('1', '2')).toEqual('0.5');
60 | expect(divideToString(new BigNumber(1), new BigNumber(2))).toEqual('0.5');
61 | });
62 |
63 | it('should throw if invalid value', () => {
64 | expect(() => divideToString(null as any, null as any)).toThrow();
65 | expect(() => divideToString(undefined as any, undefined as any)).toThrow();
66 | expect(() => divideToString('', '')).toThrow();
67 | });
68 | });
69 |
70 | describe('oppositeToString', () => {
71 | it('should return expected value.', () => {
72 | expect(oppositeToString(1)).toEqual('-1');
73 | expect(oppositeToString(-1)).toEqual('1');
74 | expect(oppositeToString('1')).toEqual('-1');
75 | expect(oppositeToString('-1')).toEqual('1');
76 | expect(oppositeToString(new BigNumber(1))).toEqual('-1');
77 | expect(oppositeToString(new BigNumber(-1))).toEqual('1');
78 | });
79 |
80 | it('should throw if invalid value', () => {
81 | expect(() => oppositeToString(null as any)).toThrow();
82 | expect(() => oppositeToString(undefined as any)).toThrow();
83 | expect(() => oppositeToString('')).toThrow();
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/libs/common/big-number/calculations.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 |
3 | import { getBigNumberStrictly } from './get-big-number';
4 |
5 | /**
6 | * v1 + v2
7 | * throw errors.
8 | *
9 | * @param {BigNumber.Value} v1
10 | * @param {BigNumber.Value[]} v2
11 | * @returns {BigNumber}
12 | */
13 | export function add(v1: BigNumber.Value, ...v2: BigNumber.Value[]): BigNumber {
14 | let result = getBigNumberStrictly(v1);
15 | for (const v of v2) {
16 | const b = getBigNumberStrictly(v);
17 | result = result.plus(b);
18 | }
19 |
20 | return result;
21 | }
22 |
23 | /**
24 | * v1 + v2
25 | * throw errors.
26 | *
27 | * @param {BigNumber.Value} v1
28 | * @param {BigNumber.Value[]} v2
29 | * @returns {number}
30 | */
31 | export function addToString(v1: BigNumber.Value, ...v2: BigNumber.Value[]): string {
32 | return add(v1, ...v2).toString();
33 | }
34 |
35 | /**
36 | * v1 - v2
37 | * throw errors.
38 | *
39 | * @param {BigNumber.Value} v1
40 | * @param {BigNumber.Value[]} v2
41 | * @returns {BigNumber}
42 | */
43 | export function subtract(v1: BigNumber.Value, ...v2: BigNumber.Value[]): BigNumber {
44 | let result = getBigNumberStrictly(v1);
45 | for (const v of v2) {
46 | const b = getBigNumberStrictly(v);
47 | result = result.minus(b);
48 | }
49 |
50 | return result;
51 | }
52 |
53 | /**
54 | * v1 - v2
55 | * throw errors.
56 | *
57 | * @param {BigNumber.Value} v1
58 | * @param {BigNumber.Value[]} v2
59 | * @returns {string}
60 | */
61 | export function subtractToString(v1: BigNumber.Value, ...v2: BigNumber.Value[]): string {
62 | return subtract(v1, ...v2).toString();
63 | }
64 |
65 | /**
66 | * v1 * v2
67 | * throw errors.
68 | *
69 | * @param {BigNumber.Value} v1
70 | * @param {BigNumber.Value[]} v2
71 | * @returns {BigNumber}
72 | */
73 | export function multiple(v1: BigNumber.Value, ...v2: BigNumber.Value[]): BigNumber {
74 | let result = getBigNumberStrictly(v1);
75 | for (const v of v2) {
76 | const b = getBigNumberStrictly(v);
77 | result = result.multipliedBy(b);
78 | }
79 |
80 | return result;
81 | }
82 |
83 | /**
84 | * v1 * v2
85 | * throw errors.
86 | *
87 | * @param {BigNumber.Value} v1
88 | * @param {BigNumber.Value} v2
89 | * @returns {number}
90 | */
91 | export function multipleToString(v1: BigNumber.Value, ...v2: BigNumber.Value[]): string {
92 | return multiple(v1, ...v2).toString();
93 | }
94 |
95 | /**
96 | * v1 / v2
97 | * throw errors.
98 | *
99 | * @param {BigNumber.Value} v1
100 | * @param {BigNumber.Value[]} v2
101 | * @returns {BigNumber}
102 | */
103 | export function divide(v1: BigNumber.Value, ...v2: BigNumber.Value[]): BigNumber {
104 | let result = getBigNumberStrictly(v1);
105 | for (const v of v2) {
106 | const b = getBigNumberStrictly(v);
107 | result = result.dividedBy(b);
108 | }
109 |
110 | return result;
111 | }
112 |
113 | /**
114 | * v1 / v2
115 | * throw errors.
116 | *
117 | * @param {BigNumber.Value} v1
118 | * @param {BigNumber.Value[]} v2
119 | * @returns {number}
120 | */
121 | export function divideToString(v1: BigNumber.Value, ...v2: BigNumber.Value[]): string {
122 | return divide(v1, ...v2).toString();
123 | }
124 |
125 | export function opposite(v: BigNumber.Value): BigNumber {
126 | return getBigNumberStrictly(v).multipliedBy(-1);
127 | }
128 |
129 | export function oppositeToString(v: BigNumber.Value): string {
130 | return opposite(v).toString();
131 | }
132 |
--------------------------------------------------------------------------------
/libs/common/big-number/compare.spec.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 |
3 | import { max, min, sum } from './compare';
4 |
5 | describe('BigNumber Util Compare Functions', () => {
6 | describe('max', () => {
7 | describe('When valid arguments', () => {
8 | describe('When use variable arguments', () => {
9 | it('should handle values as expected', () => {
10 | expect(max(1, 2, 3)).toBe(3);
11 | expect(max(1, 2, '3')).toBe('3');
12 | });
13 | });
14 |
15 | describe('When use single array argument', () => {
16 | it('should handle values as expected', () => {
17 | expect(max([1, 2, 3])).toBe(3);
18 | expect(max([1, 2, '3'])).toBe('3');
19 | });
20 | });
21 | });
22 |
23 | describe('When invalid arguments', () => {
24 | it('should throw error', () => {
25 | expect(() => max('', 2, 3)).toThrow();
26 | expect(() => max(null as any, 2, 3)).toThrow();
27 | expect(() => max(undefined as any, 2, 3)).toThrow();
28 | expect(() => max(1, 2, NaN as any)).toThrow();
29 | });
30 | });
31 | });
32 |
33 | describe('min', () => {
34 | describe('When valid arguments', () => {
35 | describe('When use variable arguments', () => {
36 | it('should handle values as expected', () => {
37 | expect(min(1, 2, 3)).toBe(1);
38 | expect(min('1', 2, 3)).toBe('1');
39 | });
40 | });
41 |
42 | describe('When use single array argument', () => {
43 | it('should handle values as expected', () => {
44 | expect(min([1, 2, 3])).toBe(1);
45 | expect(min(['1', 2, 3])).toBe('1');
46 | });
47 | });
48 | });
49 |
50 | describe('When invalid arguments', () => {
51 | it('should throw error', () => {
52 | expect(() => min('', 2, 3)).toThrow();
53 | expect(() => min(null as any, 2, 3)).toThrow();
54 | expect(() => min(undefined as any, 2, 3)).toThrow();
55 | expect(() => min(1, 2, NaN as any)).toThrow();
56 | });
57 | });
58 | });
59 |
60 | describe('sum', () => {
61 | describe('When valid arguments', () => {
62 | describe('When use variable arguments', () => {
63 | it('should handle values as expected', () => {
64 | expect(sum(1, 2, 3)).toEqual(new BigNumber('6'));
65 | expect(sum('1', 2, 3)).toEqual(new BigNumber('6'));
66 | });
67 | });
68 |
69 | describe('When use single array argument', () => {
70 | it('should handle values as expected', () => {
71 | expect(sum([1, 2, 3])).toEqual(new BigNumber('6'));
72 | expect(sum(['1', 2, 3])).toEqual(new BigNumber('6'));
73 | });
74 | });
75 | });
76 |
77 | describe('When invalid arguments', () => {
78 | it('should throw error', () => {
79 | expect(() => sum('', 2, 3)).toThrow();
80 | expect(() => sum(null as any, 2, 3)).toThrow();
81 | expect(() => sum(undefined as any, 2, 3)).toThrow();
82 | expect(() => sum(1, 2, NaN as any)).toThrow();
83 | });
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/libs/common/big-number/compare.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 |
3 | /**
4 | * To use variable arguments.
5 | *
6 | * `max(1, 2, 3);`
7 | */
8 | type VariableArguments = BigNumber.Value[];
9 |
10 | /**
11 | * To use single array argument.
12 | *
13 | * `max([1, 2, 3]);`
14 | */
15 | type SingleArrayArgument = [BigNumber.Value[]];
16 |
17 | /**
18 | * Get the max value in arguments.
19 | *
20 | * Return undefined if arguments includes invalid one.
21 | *
22 | * @param params
23 | */
24 | export function maxOrUndefined(...params: VariableArguments | SingleArrayArgument): BigNumber.Value | undefined {
25 | const useArray = Array.isArray(params[0]);
26 | const values = (useArray ? params[0] : params) as BigNumber.Value[];
27 |
28 | const maximum = BigNumber.maximum.apply(null, values);
29 |
30 | return values.find((v) => maximum.isEqualTo(v))!;
31 | }
32 |
33 | /**
34 | * Get the max value in arguments.
35 | *
36 | * Throw error if arguments includes invalid one.
37 | *
38 | * @param params
39 | */
40 | export function max(...params: VariableArguments | SingleArrayArgument): BigNumber.Value {
41 | const result = maxOrUndefined(...params);
42 | if (result === undefined) {
43 | throw new Error('Non numeric value passes');
44 | }
45 |
46 | return result;
47 | }
48 |
49 | /**
50 | * Get the min value in arguments.
51 | *
52 | * Return undefined if arguments includes invalid one.
53 | *
54 | * @param params
55 | */
56 | export function minOrUndefined(...params: VariableArguments | SingleArrayArgument): BigNumber.Value | undefined {
57 | const useArray = Array.isArray(params[0]);
58 | const values = (useArray ? params[0] : params) as BigNumber.Value[];
59 |
60 | const maximum = BigNumber.minimum.apply(null, values);
61 |
62 | return values.find((v) => maximum.isEqualTo(v))!;
63 | }
64 |
65 | /**
66 | * Get the min value in arguments.
67 | *
68 | * Throw error if arguments includes invalid one.
69 | *
70 | * @param params
71 | */
72 | export function min(...params: VariableArguments | SingleArrayArgument): BigNumber.Value {
73 | const result = minOrUndefined(...params);
74 | if (result === undefined) {
75 | throw new Error('Non numeric value passes');
76 | }
77 |
78 | return result;
79 | }
80 |
81 | /**
82 | * Get the sum value in arguments.
83 | *
84 | * Return NaN:number if arguments includes invalid one.
85 | *
86 | * @param params
87 | */
88 | export function sumOrNaN(...params: VariableArguments | SingleArrayArgument): BigNumber.Value | number {
89 | const useArray = Array.isArray(params[0]);
90 | const values = (useArray ? params[0] : params) as BigNumber.Value[];
91 |
92 | let sumv = new BigNumber(0);
93 | for (const v of values) {
94 | if (!new BigNumber(v).isEqualTo(v)) {
95 | return NaN;
96 | }
97 | sumv = sumv.plus(v);
98 | }
99 |
100 | return sumv;
101 | }
102 |
103 | /**
104 | * Get the sum value in arguments.
105 | *
106 | * Throw error if arguments includes invalid one.
107 | *
108 | * @param params
109 | */
110 | export function sum(...params: VariableArguments | SingleArrayArgument): BigNumber.Value {
111 | const result = sumOrNaN(...params);
112 | if (!BigNumber.isBigNumber(result)) {
113 | throw new Error('Non numeric value passes');
114 | }
115 |
116 | return result;
117 | }
118 |
--------------------------------------------------------------------------------
/libs/common/big-number/condition.spec.ts:
--------------------------------------------------------------------------------
1 | import { equal, gt, gte, isFiniteStrictly, isNegative, isPositive, isZero, lt, lte } from './condition';
2 |
3 | describe('BigNumber Util Condition Functions', () => {
4 | describe('equal', () => {
5 | it('should handle value as expected', () => {
6 | expect(equal(1, 1)).toBe(1 === 1);
7 | });
8 | it('should throw error whenever non numeric value', () => {
9 | expect(() => equal(null as any, 1)).toThrow();
10 | expect(() => equal(NaN as any, 1)).toThrow();
11 | });
12 | });
13 |
14 | describe('gt', () => {
15 | it('should handle value as expected', () => {
16 | expect(gt(2, 1)).toBe(2 > 1);
17 | });
18 | it('should throw error whenever non numeric value', () => {
19 | expect(() => gt(null as any, 1)).toThrow();
20 | });
21 | });
22 |
23 | describe('lt', () => {
24 | it('should handle value as expected', () => {
25 | expect(lt(1, 2)).toBe(1 < 2);
26 | });
27 | it('should throw error whenever non numeric value', () => {
28 | expect(() => lt(null as any, 1)).toThrow();
29 | });
30 | });
31 |
32 | describe('gte', () => {
33 | it('should handle value as expected', () => {
34 | expect(gte(2, 1)).toBe(2 > 1);
35 | });
36 | it('should throw error whenever non numeric value', () => {
37 | expect(() => gte(null as any, 1)).toThrow();
38 | });
39 | });
40 |
41 | describe('lte', () => {
42 | it('should handle value as expected', () => {
43 | expect(lte(1, 2)).toBe(1 < 2);
44 | });
45 | it('should throw error whenever non numeric value', () => {
46 | expect(() => lte(null as any, 1)).toThrow();
47 | });
48 | });
49 |
50 | describe('isPositive', () => {
51 | it('should handle value as expected', () => {
52 | expect(isPositive(1)).toBe(0 < 1);
53 | });
54 | it('should throw error whenever non numeric value', () => {
55 | expect(() => isPositive(null as any)).toThrow();
56 | });
57 | });
58 |
59 | describe('isNegative', () => {
60 | it('should handle value as expected', () => {
61 | expect(isNegative(-1)).toBe(-1 < 0);
62 | });
63 | it('should throw error whenever non numeric value', () => {
64 | expect(() => isNegative(null as any)).toThrow();
65 | });
66 | });
67 |
68 | describe('isZero', () => {
69 | it('should handle value as expected', () => {
70 | expect(isZero(0)).toBe(0 === 0);
71 | });
72 | it('should throw error whenever non numeric value', () => {
73 | expect(() => isZero(null as any)).toThrow();
74 | });
75 | });
76 |
77 | describe('isFiniteStrictly', () => {
78 | it('should handle value as expected', () => {
79 | expect(isFiniteStrictly(1)).toBe(Number.isFinite(1));
80 | });
81 | it('should throw error whenever non numeric value', () => {
82 | expect(() => isFiniteStrictly(null as any)).toThrow();
83 | });
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/libs/common/big-number/condition.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 |
3 | import { getBigNumberStrictly } from './get-big-number';
4 |
5 | export function equal(v1: BigNumber.Value, v2: BigNumber.Value): boolean {
6 | const a = getBigNumberStrictly(v1);
7 | const b = getBigNumberStrictly(v2);
8 |
9 | return a.isEqualTo(b);
10 | }
11 |
12 | export function gt(v1: BigNumber.Value, v2: BigNumber.Value): boolean {
13 | const a = getBigNumberStrictly(v1);
14 | const b = getBigNumberStrictly(v2);
15 |
16 | return a.gt(b);
17 | }
18 |
19 | export function lt(v1: BigNumber.Value, v2: BigNumber.Value): boolean {
20 | const a = getBigNumberStrictly(v1);
21 | const b = getBigNumberStrictly(v2);
22 |
23 | return a.lt(b);
24 | }
25 |
26 | export function gte(v1: BigNumber.Value, v2: BigNumber.Value): boolean {
27 | const a = getBigNumberStrictly(v1);
28 | const b = getBigNumberStrictly(v2);
29 |
30 | return a.gte(b);
31 | }
32 |
33 | export function lte(v1: BigNumber.Value, v2: BigNumber.Value): boolean {
34 | const a = getBigNumberStrictly(v1);
35 | const b = getBigNumberStrictly(v2);
36 |
37 | return a.lte(b);
38 | }
39 |
40 | export function isPositive(v: BigNumber.Value): boolean {
41 | const bn = getBigNumberStrictly(v);
42 |
43 | return bn.isPositive() && bn.isGreaterThan(0);
44 | }
45 |
46 | export function isNegative(v: BigNumber.Value): boolean {
47 | const bn = getBigNumberStrictly(v);
48 |
49 | return bn.isNegative() && bn.isLessThan(0);
50 | }
51 |
52 | export function isZero(v: BigNumber.Value): boolean {
53 | return getBigNumberStrictly(v).isZero();
54 | }
55 |
56 | export function isFiniteStrictly(v: BigNumber.Value): boolean {
57 | return getBigNumberStrictly(v).isFinite();
58 | }
59 |
--------------------------------------------------------------------------------
/libs/common/big-number/format.spec.ts:
--------------------------------------------------------------------------------
1 | import { abs, absToString, ceilToString, fix, floorToFixed, floorToString, stripZero } from './format';
2 |
3 | describe('floorToString', () => {
4 | describe('for integer', () => {
5 | it('should return expected result', () => {
6 | // big integer
7 | expect(floorToString('100000000000000000000000000')).toBe('1e+26');
8 | expect(floorToString(0.1)).toBe('0');
9 | expect(floorToString('0.0000000000000000000000001')).toBe('0');
10 | });
11 | });
12 |
13 | describe('for decimal', () => {
14 | it('should return expected result', () => {
15 | expect(floorToString(1.23456789, 0)).toBe('1');
16 | expect(floorToString(1.23456789, 1)).toBe('1.2');
17 | expect(floorToString(1.23456789, 2)).toBe('1.23');
18 | expect(floorToString(1.23456789, 3)).toBe('1.234');
19 | expect(floorToString(1.23456789, 4)).toBe('1.2345');
20 | expect(floorToString(1.23456789, 5)).toBe('1.23456');
21 | expect(floorToString(1.23456789, 6)).toBe('1.234567');
22 | expect(floorToString(1.23456789, 7)).toBe('1.2345678');
23 | expect(floorToString(1.23456789, 8)).toBe('1.23456789');
24 | });
25 | });
26 | });
27 |
28 | describe('floorToFixed', () => {
29 | describe('When proper values are provided', () => {
30 | it('should calculate values properly', () => {
31 | expect(floorToFixed('1.23456789', 0)).toBe('1');
32 | expect(floorToFixed('1.234567890', 7)).toBe('1.2345678');
33 | expect(floorToFixed('1.234567890', 8)).toBe('1.23456789');
34 | expect(floorToFixed('1.234567890', 9)).toBe('1.234567890');
35 | });
36 | });
37 | });
38 |
39 | describe('ceil', () => {
40 | describe('When proper values are provided', () => {
41 | it('should calculate values properly', () => {
42 | expect(ceilToString('1.23456789', 0)).toBe('2');
43 | expect(ceilToString('1.234567890', 1)).toBe('1.3');
44 | expect(ceilToString('1.234567890', 2)).toBe('1.24');
45 | expect(ceilToString('1.234567890', 3)).toBe('1.235');
46 | expect(ceilToString('1.234567890', 4)).toBe('1.2346');
47 | expect(ceilToString('1.234567890', 5)).toBe('1.23457');
48 | expect(ceilToString('1.234567890', 6)).toBe('1.234568');
49 | expect(ceilToString('1.234567890', 7)).toBe('1.2345679');
50 | expect(ceilToString('1.234567890', 8)).toBe('1.23456789');
51 | expect(ceilToString('1.234567890', 9)).toBe('1.23456789');
52 | });
53 | });
54 | });
55 |
56 | describe('fix', () => {
57 | describe('When proper values are provided', () => {
58 | it('should calculate values properly', () => {
59 | const base = '0.001';
60 | expect(fix(base, 1)).toBe('0.0');
61 | expect(fix(base, 2)).toBe('0.00');
62 | expect(fix(base, 3)).toBe('0.001');
63 | expect(fix(base, 4)).toBe('0.0010');
64 | });
65 |
66 | it('should round values', () => {
67 | const base = '0.009';
68 | expect(fix(base, 2)).toBe('0.01');
69 | });
70 | });
71 | });
72 |
73 | describe('stripZero', () => {
74 | it('should work', () => {
75 | expect(stripZero('1.000000000000000000000001')).toBe('1.000000000000000000000001');
76 | expect(stripZero('1.000000000000000000000000')).toBe('1');
77 | expect(stripZero('10000000000000')).toBe('10000000000000');
78 | });
79 | });
80 |
81 | describe('abs', () => {
82 | it('should work', () => {
83 | expect(abs(1).toNumber()).toBe(1);
84 | expect(abs(-1).toNumber()).toBe(1);
85 | });
86 | });
87 |
88 | describe('absToString', () => {
89 | it('should work', () => {
90 | expect(absToString(1)).toBe('1');
91 | expect(absToString(-1)).toBe('1');
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/libs/common/big-number/format.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 |
3 | import { getBigNumberStrictly } from './get-big-number';
4 |
5 | /**
6 | * get floored number by specific decimal places.
7 | *
8 | * @param {number} value - number to floor.
9 | * @param {number} decimalPlace - where to floor.
10 | * @returns {number}
11 | */
12 | export function floor(value: BigNumber.Value, decimalPlace = 0): BigNumber {
13 | if (decimalPlace === 0) {
14 | return getBigNumberStrictly(value).integerValue(BigNumber.ROUND_FLOOR);
15 | }
16 |
17 | const characteristic = getBigNumberStrictly(10).pow(decimalPlace);
18 |
19 | return getBigNumberStrictly(value).multipliedBy(characteristic).integerValue(BigNumber.ROUND_FLOOR).dividedBy(characteristic);
20 | }
21 |
22 | /**
23 | * get floored number by specific decimal places.
24 | *
25 | * @param {number} value - number to floor.
26 | * @param {number} decimalPlace - where to floor.
27 | * @returns {number}
28 | */
29 | export function floorToString(value: BigNumber.Value, decimalPlace = 0): string {
30 | return floor(value, decimalPlace).toString();
31 | }
32 |
33 | /**
34 | * get floored and fixed value by specific decimal places.
35 | *
36 | * @param value
37 | * @param decimalPlace
38 | */
39 | export function floorToFixed(value: BigNumber.Value, decimalPlace = 0): string {
40 | return floor(value, decimalPlace).toFixed(decimalPlace);
41 | }
42 |
43 | /**
44 | * get celled number by specific decimal places.
45 | *
46 | * @param {number} value - number to floor.
47 | * @param {number} decimalPlace - where to floor.
48 | * @returns {number}
49 | */
50 | export function ceil(value: BigNumber.Value, decimalPlace = 0): BigNumber {
51 | if (decimalPlace === 0) {
52 | return getBigNumberStrictly(value).integerValue(BigNumber.ROUND_CEIL);
53 | }
54 |
55 | const characteristic = getBigNumberStrictly(10).pow(decimalPlace);
56 |
57 | return getBigNumberStrictly(value).multipliedBy(characteristic).integerValue(BigNumber.ROUND_CEIL).dividedBy(characteristic);
58 | }
59 |
60 | /**
61 | * get celled number by specific decimal places.
62 | *
63 | * @param {number} value - number to floor.
64 | * @param {number} decimalPlace - where to floor.
65 | * @returns {number}
66 | */
67 | export function ceilToString(value: BigNumber.Value, decimalPlace = 0): string {
68 | return ceil(value, decimalPlace).toString();
69 | }
70 |
71 | /**
72 | * Fix value with provided digits.
73 | *
74 | * @param value
75 | * @param digits
76 | */
77 | export function fix(value: BigNumber.Value, digits: number): string {
78 | const bn = getBigNumberStrictly(value);
79 |
80 | return bn.toFixed(digits);
81 | }
82 |
83 | export const stripZero = (v: BigNumber.Value) => {
84 | return getBigNumberStrictly(v).toString();
85 | };
86 |
87 | /**
88 | * Absolute value.
89 | *
90 | * @param value
91 | * @returns BigNumber
92 | */
93 | export function abs(value: BigNumber.Value): BigNumber {
94 | const bn = getBigNumberStrictly(value);
95 |
96 | return bn.abs();
97 | }
98 |
99 | /**
100 | * Absolute value.
101 | *
102 | * @param value
103 | * @returns string
104 | */
105 | export function absToString(value: BigNumber.Value): string {
106 | const bn = getBigNumberStrictly(value);
107 |
108 | return bn.abs().toString();
109 | }
110 |
--------------------------------------------------------------------------------
/libs/common/big-number/get-big-number.spec.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 |
3 | import { getBigNumber, getBigNumberStrictly } from './get-big-number';
4 |
5 | describe('BigNumber Util Functions', () => {
6 | describe('getBigNumber', () => {
7 | it('should return instance for acceptable values', () => {
8 | expect(getBigNumber(0).toNumber()).toBe(0);
9 | expect(getBigNumber(-0).toNumber()).toBe(-0);
10 | expect(getBigNumber(1).toNumber()).toBe(1);
11 | expect(getBigNumber(-1).toNumber()).toBe(-1);
12 | expect(getBigNumber(10000000000000000000000).toNumber()).toBe(10000000000000000000000);
13 | expect(getBigNumber(-10000000000000000000000).toNumber()).toBe(-10000000000000000000000);
14 | expect(getBigNumber(0.000000000000000000001).toNumber()).toBe(0.000000000000000000001);
15 | expect(getBigNumber(-0.000000000000000000001).toNumber()).toBe(-0.000000000000000000001);
16 | expect(getBigNumber(Infinity).toNumber()).toBe(Infinity);
17 | expect(getBigNumber(-Infinity).toNumber()).toBe(-Infinity);
18 |
19 | expect(getBigNumber('0').toString()).toBe('0');
20 | expect(getBigNumber('-0').toString()).toBe('0');
21 | expect(getBigNumber('1').toString()).toBe('1');
22 | expect(getBigNumber('-1').toString()).toBe('-1');
23 | expect(getBigNumber('10000000000000000000000').toString()).toBe('1e+22');
24 | expect(getBigNumber('-10000000000000000000000').toString()).toBe('-1e+22');
25 | expect(getBigNumber('0.000000000000000000001').toString()).toBe('1e-21');
26 | expect(getBigNumber('-0.000000000000000000001').toString()).toBe('-1e-21');
27 | expect(getBigNumber('Infinity').toString()).toBe('Infinity');
28 | expect(getBigNumber('-Infinity').toString()).toBe('-Infinity');
29 |
30 | expect(getBigNumber(new BigNumber('0')).toString()).toBe('0');
31 | expect(getBigNumber(new BigNumber(getBigNumber('-0'))).toString()).toBe('0');
32 | expect(getBigNumber(new BigNumber(getBigNumber('1'))).toString()).toBe('1');
33 | expect(getBigNumber(new BigNumber(getBigNumber('-1'))).toString()).toBe('-1');
34 | expect(getBigNumber(new BigNumber(getBigNumber('10000000000000000000000'))).toString()).toBe('1e+22');
35 | expect(getBigNumber(new BigNumber(getBigNumber('-10000000000000000000000'))).toString()).toBe('-1e+22');
36 | expect(getBigNumber(new BigNumber(getBigNumber('0.000000000000000000001'))).toString()).toBe('1e-21');
37 | expect(getBigNumber(new BigNumber(getBigNumber('-0.000000000000000000001'))).toString()).toBe('-1e-21');
38 | expect(getBigNumber(new BigNumber(getBigNumber('Infinity'))).toString()).toBe('Infinity');
39 | expect(getBigNumber(new BigNumber(getBigNumber('-Infinity'))).toString()).toBe('-Infinity');
40 | });
41 |
42 | it('should return instance for unacceptable values.', () => {
43 | expect(getBigNumber('').toNumber()).toEqual(NaN);
44 | expect(getBigNumber('string').toNumber()).toEqual(NaN);
45 | expect(getBigNumber(null as any).toNumber()).toEqual(NaN);
46 | expect(getBigNumber(undefined as any).toNumber()).toEqual(NaN);
47 | expect(getBigNumber([] as any).toNumber()).toEqual(NaN);
48 | expect(getBigNumber({} as any).toNumber()).toEqual(NaN);
49 | });
50 | });
51 |
52 | describe('getBigNumberSafely', () => {
53 | it('should not throw error when acceptable value', () => {
54 | expect(() => getBigNumberStrictly(0)).not.toThrow();
55 | expect(() => getBigNumberStrictly(-0)).not.toThrow();
56 | expect(() => getBigNumberStrictly(1)).not.toThrow();
57 | expect(() => getBigNumberStrictly(-1)).not.toThrow();
58 | expect(() => getBigNumberStrictly(10000000000000000000000)).not.toThrow();
59 | expect(() => getBigNumberStrictly(-10000000000000000000000)).not.toThrow();
60 | expect(() => getBigNumberStrictly(0.000000000000000000001)).not.toThrow();
61 | expect(() => getBigNumberStrictly(-0.000000000000000000001)).not.toThrow();
62 | expect(() => getBigNumberStrictly(Infinity)).not.toThrow();
63 | expect(() => getBigNumberStrictly(-Infinity)).not.toThrow();
64 |
65 | expect(() => getBigNumberStrictly('0')).not.toThrow();
66 | expect(() => getBigNumberStrictly('-0')).not.toThrow();
67 | expect(() => getBigNumberStrictly('1')).not.toThrow();
68 | expect(() => getBigNumberStrictly('-1')).not.toThrow();
69 | expect(() => getBigNumberStrictly('10000000000000000000000')).not.toThrow();
70 | expect(() => getBigNumberStrictly('-10000000000000000000000')).not.toThrow();
71 | expect(() => getBigNumberStrictly('0.000000000000000000001')).not.toThrow();
72 | expect(() => getBigNumberStrictly('-0.000000000000000000001')).not.toThrow();
73 | expect(() => getBigNumberStrictly('Infinity')).not.toThrow();
74 | expect(() => getBigNumberStrictly('-Infinity')).not.toThrow();
75 |
76 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('0')))).not.toThrow();
77 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('-0')))).not.toThrow();
78 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('1')))).not.toThrow();
79 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('-1')))).not.toThrow();
80 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('10000000000000000000000')))).not.toThrow();
81 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('-10000000000000000000000')))).not.toThrow();
82 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('0.000000000000000000001')))).not.toThrow();
83 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('-0.000000000000000000001')))).not.toThrow();
84 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('Infinity')))).not.toThrow();
85 | expect(() => getBigNumberStrictly(new BigNumber(getBigNumberStrictly('-Infinity')))).not.toThrow();
86 | });
87 |
88 | it('should throw error when unacceptable value', () => {
89 | expect(() => getBigNumberStrictly('')).toThrow();
90 | expect(() => getBigNumberStrictly('string')).toThrow();
91 | expect(() => getBigNumberStrictly(null as any)).toThrow();
92 | expect(() => getBigNumberStrictly(undefined as any)).toThrow();
93 | expect(() => getBigNumberStrictly([] as any)).toThrow();
94 | expect(() => getBigNumberStrictly({} as any)).toThrow();
95 | });
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/libs/common/big-number/get-big-number.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 |
3 | export function getBigNumber(value: BigNumber.Value): BigNumber {
4 | let bn: BigNumber;
5 |
6 | try {
7 | bn = new BigNumber(value);
8 | } catch (e) {
9 | bn = new BigNumber(NaN);
10 | }
11 |
12 | return bn;
13 | }
14 |
15 | export function getBigNumberStrictly(value: BigNumber.Value): BigNumber {
16 | const bn = getBigNumber(value);
17 |
18 | if (bn.isNaN()) {
19 | throw new Error('value is not numeric.');
20 | }
21 |
22 | return bn;
23 | }
24 |
--------------------------------------------------------------------------------
/libs/common/big-number/index.ts:
--------------------------------------------------------------------------------
1 | export * from './compare';
2 | export * from './condition';
3 | export * from './format';
4 | export * from './calculations';
5 | export * from './get-big-number';
6 | export * from './validation';
7 |
--------------------------------------------------------------------------------
/libs/common/big-number/validation.ts:
--------------------------------------------------------------------------------
1 | import { getBigNumberStrictly } from './get-big-number';
2 |
3 | enum ErrorMessage {
4 | NotFinite = 'value is not finite number.',
5 | NotZero = 'value is not zero.',
6 | NotPositive = 'value is not positive number.',
7 | NotNegative = 'value is not negative number.',
8 | NotInteger = 'value is not integer.',
9 | NotGTEZero = 'value is not gte 0',
10 | NotLTEZero = 'value is not lte 0',
11 | }
12 |
13 | export function validateFiniteNumber(value: any): void {
14 | const bn = getBigNumberStrictly(value);
15 | if (!bn.isFinite()) {
16 | throw new Error(ErrorMessage.NotFinite);
17 | }
18 | }
19 |
20 | export function validateZero(value: any): void {
21 | const bn = getBigNumberStrictly(value);
22 | if (!bn.eq(0)) {
23 | throw new Error(ErrorMessage.NotZero);
24 | }
25 | }
26 |
27 | export function validatePositiveNumber(value: any): void {
28 | const bn = getBigNumberStrictly(value);
29 | if (!bn.gt(0)) {
30 | throw new Error(ErrorMessage.NotPositive);
31 | }
32 | }
33 |
34 | export function validateNegativeNumber(value: any): void {
35 | const bn = getBigNumberStrictly(value);
36 | if (!bn.lt(0)) {
37 | throw new Error(ErrorMessage.NotNegative);
38 | }
39 | }
40 |
41 | export function validatePositiveFiniteNumber(value: any): void {
42 | const bn = getBigNumberStrictly(value);
43 | if (!bn.isFinite()) {
44 | throw new Error(ErrorMessage.NotFinite);
45 | }
46 | if (!bn.gt(0)) {
47 | throw new Error(ErrorMessage.NotPositive);
48 | }
49 | }
50 |
51 | export function validateNegativeFiniteNumber(value: any): void {
52 | const bn = getBigNumberStrictly(value);
53 | if (!bn.isFinite()) {
54 | throw new Error(ErrorMessage.NotFinite);
55 | }
56 | if (!bn.lt(0)) {
57 | throw new Error(ErrorMessage.NotNegative);
58 | }
59 | }
60 |
61 | export function validatePositiveFiniteNumberOrZero(value: any): void {
62 | const bn = getBigNumberStrictly(value);
63 | if (!bn.isFinite()) {
64 | throw new Error(ErrorMessage.NotFinite);
65 | }
66 | if (!bn.gte(0)) {
67 | throw new Error(ErrorMessage.NotGTEZero);
68 | }
69 | }
70 |
71 | export function validateNegativeFiniteNumberOrZero(value: any): void {
72 | const bn = getBigNumberStrictly(value);
73 | if (!bn.isFinite()) {
74 | throw new Error(ErrorMessage.NotFinite);
75 | }
76 | if (!bn.lte(0)) {
77 | throw new Error(ErrorMessage.NotLTEZero);
78 | }
79 | }
80 |
81 | export function validateIntegerNumber(value: any): void {
82 | const bn = getBigNumberStrictly(value);
83 | if (!bn.isInteger()) {
84 | throw new Error(ErrorMessage.NotInteger);
85 | }
86 | }
87 |
88 | export function validatePositiveIntegerNumber(value: any): void {
89 | const bn = getBigNumberStrictly(value);
90 | if (!bn.isInteger()) {
91 | throw new Error(ErrorMessage.NotInteger);
92 | }
93 | if (!bn.gte(0)) {
94 | throw new Error(ErrorMessage.NotPositive);
95 | }
96 | }
97 |
98 | export function validateNegativeIntegerNumber(value: any): void {
99 | const bn = getBigNumberStrictly(value);
100 | if (!bn.isInteger()) {
101 | throw new Error(ErrorMessage.NotInteger);
102 | }
103 | if (!bn.lte(0)) {
104 | throw new Error(ErrorMessage.NotNegative);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/libs/common/descriptors/catch-error-decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { useFirstValue } from '@ta2-libs/common';
2 | import { Observable } from 'rxjs';
3 |
4 | import { DefaultExceptionHandler } from '../../exceptions';
5 | import { CatchError } from './catch-error.decorator';
6 |
7 | @CatchError(DefaultExceptionHandler)
8 | class Foo {
9 | func() {
10 | throw new Error('func error');
11 | }
12 |
13 | func2(param: any) {
14 | return param;
15 | }
16 |
17 | funcPromise(): Promise {
18 | return new Promise(() => {
19 | throw new Error('funcPromise error');
20 | });
21 | }
22 |
23 | funcObservable(): Observable {
24 | return new Observable((observer) => {
25 | try {
26 | throw new Error('funcObservable error');
27 | observer.next(1);
28 | } catch (err) {
29 | observer.error(err);
30 | } finally {
31 | observer.complete();
32 | }
33 | });
34 | }
35 | }
36 |
37 | describe('DefaultExceptionFilter', () => {
38 | const foo = new Foo();
39 |
40 | it('func', () => {
41 | expect(foo.func());
42 | });
43 |
44 | it('func2', () => {
45 | const data = { hogehoge: 1001 };
46 | expect(foo.func2(data)).toEqual(data);
47 | });
48 |
49 | it('funcPromise', async () => {
50 | await expect(foo.funcPromise());
51 | });
52 |
53 | it('funcObservable', async () => {
54 | await expect(useFirstValue(foo.funcObservable()));
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/libs/common/descriptors/catch-error.decorator.ts:
--------------------------------------------------------------------------------
1 | import { of } from 'rxjs';
2 | import { catchError } from 'rxjs/operators';
3 |
4 | import { ExceptionHandler } from '../../exceptions';
5 |
6 | export function CatchError(handler: ExceptionHandler): Function {
7 | return function (target: Function) {
8 | const prototype = target.prototype;
9 | const className = target.name;
10 | const methods = Object.getOwnPropertyNames(prototype).filter((prop) => typeof prototype[prop] === 'function');
11 | methods.forEach(function (methodName: string): void {
12 | const originalMethod = prototype[methodName];
13 | prototype[methodName] = function (...args) {
14 | try {
15 | const result = originalMethod.apply(this, args);
16 | if (result) {
17 | // is promise function
18 | if (typeof result['catch'] === 'function') {
19 | return result.catch((e) => handler.handle(className, methodName, args, e));
20 | } else if (typeof result['subscribe'] === 'function') {
21 | // is observable function
22 | return result.pipe(
23 | catchError((error) => {
24 | handler.handle(className, methodName, args, error);
25 | return of();
26 | }),
27 | );
28 | }
29 | }
30 | return result;
31 | } catch (e) {
32 | handler.handle(className, methodName, args, e);
33 | }
34 | };
35 | });
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/libs/common/descriptors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './log-caller.decorator';
2 | export * from './retry-order.decorator';
3 | export * from './catch-error.decorator';
4 |
--------------------------------------------------------------------------------
/libs/common/descriptors/log-caller.decorator.ts:
--------------------------------------------------------------------------------
1 | import { getCallerMethodName } from '../utils';
2 |
3 | export function LogCaller(): Function {
4 | return function (target: Function) {
5 | const prototype = target.prototype;
6 | const methods = Object.getOwnPropertyNames(prototype).filter(
7 | (prop) => typeof prototype[prop] === 'function' && prop !== 'constructor' && prop !== 'requestLog' && prop !== 'error',
8 | );
9 | methods.forEach(function (methodName: string): void {
10 | const originalMethod = prototype[methodName];
11 |
12 | prototype[methodName] = function (...args) {
13 | try {
14 | throw new Error();
15 | } catch (err) {
16 | const callerClassName = args[0];
17 | args.shift();
18 | const callerMethodName = getCallerMethodName(err.stack);
19 | const tag = `${callerClassName},${callerMethodName}`;
20 | return originalMethod.apply(this, [tag, ...args]);
21 | }
22 | };
23 | });
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/libs/common/descriptors/retry-order-decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionType } from '@ta2-libs/broker-api';
2 |
3 | import { RetryOrder } from './retry-order.decorator';
4 |
5 | class Foo {
6 | name = 'foo';
7 | status = 'test';
8 |
9 | @RetryOrder(1000)
10 | order(edge: any, options?: { isRetry: boolean }): any {
11 | if (options.isRetry) {
12 | console.log('is retry order');
13 | }
14 | console.log('do order...', this.status);
15 |
16 | return {
17 | info: {
18 | status: this.status,
19 | },
20 | };
21 | }
22 | }
23 |
24 | describe('RetryOrder', () => {
25 | it('func', (done: () => void) => {
26 | const foo = new Foo();
27 | expect(foo.order({}));
28 | setTimeout(() => {
29 | foo.status = ExecutionType.FILLED;
30 | }, 5000);
31 |
32 | setTimeout(() => done(), 8000);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/libs/common/descriptors/retry-order.decorator.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionType } from '@ta2-libs/broker-api/binance-api/types/ws';
2 |
3 | export interface RetryOrderOptions {
4 | isRetry: true;
5 | orderResult: any;
6 | }
7 |
8 | export function RetryOrder(delayMs: number): Function {
9 | return function (target: Function, methodName: string, descriptor: PropertyDescriptor) {
10 | try {
11 | const originalMethod = descriptor.value;
12 | descriptor.value = function (...args) {
13 | const result = originalMethod.call(this, ...args);
14 | if (result && result.info && result.info.status !== ExecutionType.FILLED) {
15 | retry.bind(this)(originalMethod, [...args, { isRetry: true, orderResult: result }], delayMs);
16 | }
17 | return result;
18 | };
19 | } catch (e) {
20 | console.error(e);
21 | }
22 |
23 | return descriptor;
24 | };
25 | }
26 |
27 | function retry(fn: Function, args: any[], delayMs: number): string {
28 | const result = fn.apply(this, args);
29 | if (result && result.info && result.info.status !== ExecutionType.FILLED) {
30 | setTimeout(() => retry.bind(this)(fn, args, delayMs), delayMs);
31 | }
32 | return result;
33 | }
34 |
--------------------------------------------------------------------------------
/libs/common/di/index.ts:
--------------------------------------------------------------------------------
1 | export * from './injection-token';
2 |
--------------------------------------------------------------------------------
/libs/common/di/injection-token.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Type } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class InjectionToken {
5 | readonly ngMetadataName = 'InjectionToken';
6 |
7 | readonly ɵprov: unknown;
8 |
9 | constructor(
10 | protected _desc: string,
11 | options?: {
12 | providedIn?: Type | 'root' | 'platform' | 'any' | null;
13 | factory: () => T;
14 | },
15 | ) {
16 | this.ɵprov = undefined;
17 | if (typeof options == 'number') {
18 | // This is a special hack to assign __NG_ELEMENT_ID__ to this instance.
19 | // See `InjectorMarkers`
20 | (this as any).__NG_ELEMENT_ID__ = options;
21 | } else if (options !== undefined) {
22 | this.ɵprov = ɵɵdefineInjectable({
23 | token: this,
24 | providedIn: options.providedIn || 'root',
25 | factory: options.factory,
26 | });
27 | }
28 | }
29 |
30 | toString(): string {
31 | return `InjectionToken ${this._desc}`;
32 | }
33 | }
34 |
35 | export function ɵɵdefineInjectable(opts: {
36 | token: unknown;
37 | providedIn?: Type | 'root' | 'platform' | 'any' | null;
38 | factory: () => T;
39 | }): unknown {
40 | return {
41 | token: opts.token,
42 | providedIn: (opts.providedIn as any) || null,
43 | factory: opts.factory,
44 | value: undefined,
45 | } as ɵɵInjectableDef;
46 | }
47 |
48 | export interface ɵɵInjectableDef {
49 | /**
50 | * Specifies that the given type belongs to a particular injector:
51 | * - `InjectorType` such as `NgModule`,
52 | * - `'root'` the root injector
53 | * - `'any'` all injectors.
54 | * - `null`, does not belong to any injector. Must be explicitly listed in the injector
55 | * `providers`.
56 | */
57 | providedIn: InjectorType | 'root' | 'platform' | 'any' | null;
58 |
59 | /**
60 | * The token to which this definition belongs.
61 | *
62 | * Note that this may not be the same as the type that the `factory` will create.
63 | */
64 | token: unknown;
65 |
66 | /**
67 | * Factory method to execute to create an instance of the injectable.
68 | */
69 | factory: (t?: Type) => T;
70 |
71 | /**
72 | * In a case of no explicit injector, a location where the instance of the injectable is stored.
73 | */
74 | value: T | undefined;
75 | }
76 |
77 | export interface InjectorType extends Type {
78 | ɵfac?: unknown;
79 | ɵinj: unknown;
80 | }
81 |
--------------------------------------------------------------------------------
/libs/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from './utils';
2 | export * from './big-number';
3 | export * from './descriptors';
4 | export * from './rx';
5 | export * from './di';
6 | export * from './time';
7 |
--------------------------------------------------------------------------------
/libs/common/rx/index.ts:
--------------------------------------------------------------------------------
1 | export * from './operators';
2 |
--------------------------------------------------------------------------------
/libs/common/rx/operators.spec.ts:
--------------------------------------------------------------------------------
1 | import { BehaviorSubject, Observable, ReplaySubject, of } from 'rxjs';
2 | import { take, tap } from 'rxjs/operators';
3 |
4 | import { flatten, retryWithDelay, useFirstValue } from './operators';
5 |
6 | describe('lib/rx/operators', () => {
7 | describe('flatten', () => {
8 | it('should work well', (done) => {
9 | const source: Observable = of([1, 2, 3, 4, 5]);
10 | const flattened: Observable = source.pipe(flatten());
11 |
12 | flattened.pipe(take(1)).subscribe((data) => {
13 | expect(data).toEqual(1);
14 | done();
15 | });
16 | });
17 | });
18 |
19 | describe('retryWithDelay', () => {
20 | it('should ignore errors twice and return value at 3rd', (done) => {
21 | let count = 0;
22 | const source = of(null).pipe(
23 | tap(() => {
24 | count++;
25 | if (count <= 3) {
26 | // 3回エラー
27 | throw new Error(`error`);
28 | }
29 | }),
30 | );
31 | const dest = source.pipe(retryWithDelay(3, 100, () => true));
32 |
33 | dest.pipe().subscribe({
34 | next: () => {
35 | expect(count).toBe(4);
36 | done();
37 | },
38 | });
39 | });
40 |
41 | it('should ignore errors 3 times and throw an error at 4th', (done) => {
42 | let count = 0;
43 | const source = of(null).pipe(
44 | tap((i) => {
45 | count++;
46 | if (count <= 4) {
47 | // 4回エラー
48 | throw new Error(`error`);
49 | }
50 | }),
51 | );
52 | const dest = source.pipe(retryWithDelay(3, 100, () => true));
53 |
54 | dest.pipe().subscribe({
55 | error: () => {
56 | expect(count).toBe(4);
57 | done();
58 | },
59 | });
60 | });
61 | });
62 |
63 | it('should throw error when predicate returns false', (done) => {
64 | let count = 0;
65 | const source = of(null).pipe(
66 | tap((i) => {
67 | count++;
68 | if (count <= 4) {
69 | // 4回エラー
70 | throw new Error(`error`);
71 | }
72 | }),
73 | );
74 | const dest = source.pipe(retryWithDelay(3, 100, () => false));
75 |
76 | dest.pipe().subscribe({
77 | error: () => {
78 | expect(count).toBe(1);
79 | done();
80 | },
81 | });
82 | });
83 |
84 | describe('useLatestValue', () => {
85 | it('should work well with BehaviorSubject', async () => {
86 | const source = new BehaviorSubject('foo');
87 | const snapshot1 = await useFirstValue(source);
88 | expect(snapshot1).toBe('foo');
89 |
90 | source.next('bar');
91 | const snapshot2 = await useFirstValue(source);
92 | expect(snapshot2).toBe('bar');
93 | });
94 |
95 | it('should work well with ReplaySubject', async () => {
96 | const source = new ReplaySubject(2);
97 | source.next('foo');
98 | source.next('bar');
99 | const snapshot = await useFirstValue(source);
100 | expect(snapshot).toBe('foo');
101 | });
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/libs/common/rx/operators.ts:
--------------------------------------------------------------------------------
1 | import { Observable, OperatorFunction, from, pipe, throwError, timer } from 'rxjs';
2 | import { concatMap, filter, mergeMap, retryWhen, take } from 'rxjs/operators';
3 |
4 | /**
5 | * Do nothing
6 | */
7 | export const noop = (): OperatorFunction => (source: Observable) => source;
8 |
9 | export const flatten = (): OperatorFunction => pipe(concatMap((items) => from(items)));
10 |
11 | export const skipNullOrUndefined = (): OperatorFunction => {
12 | function isNotNullOrUndefined(input: null | undefined | T): input is T {
13 | return input != null;
14 | }
15 |
16 | return (source$) => source$.pipe(filter(isNotNullOrUndefined));
17 | };
18 |
19 | export const retryWithDelay = (
20 | limitCount: number,
21 | intervalMs = 1000,
22 | predicate: (error: E) => boolean,
23 | ): OperatorFunction =>
24 | pipe(
25 | retryWhen((error$) =>
26 | error$.pipe(
27 | mergeMap((error, i) => {
28 | if (!predicate(error)) {
29 | return throwError(error);
30 | }
31 | if (i >= limitCount) {
32 | return throwError(error);
33 | }
34 | return timer(intervalMs);
35 | }),
36 | ),
37 | ),
38 | );
39 |
40 | export const useFirstValue = (observable: Observable): Promise => observable.pipe(take(1)).toPromise();
41 |
--------------------------------------------------------------------------------
/libs/common/time/get-timestring.ts:
--------------------------------------------------------------------------------
1 | import moment = require('moment');
2 |
3 | /**
4 | * @return 'YYYY-MM-DDTHH:mm:ss.SSSZZ'
5 | */
6 | export function getTimestring(): string {
7 | return moment().format('YYYY-MM-DDTHH:mm:ss.SSSZZ');
8 | }
9 |
--------------------------------------------------------------------------------
/libs/common/time/index.ts:
--------------------------------------------------------------------------------
1 | export * from './get-timestring';
2 |
--------------------------------------------------------------------------------
/libs/common/utils/get-caller-method-name/get-caller-method-name.spec.ts:
--------------------------------------------------------------------------------
1 | import { getCallerMethodName } from './get-caller-method-name';
2 |
3 | describe('getCallerMethodName util functions', () => {
4 | it('getCallerMethodName #1', () => {
5 | const errStack =
6 | 'Error: \n' +
7 | ' at Logger.prototype. (/Users/pro/trader/triangular-arbitrage2-pro/src/app/common/descriptors/log-caller.decorator.ts:12:17)\n' +
8 | ' at TapSubscriber._tapNext (/Users/pro/trader/triangular-arbitrage2-pro/src/app/feature-modules/engine/engine.service.ts:88:21)\n' +
9 | ' at TapSubscriber._next (/Users/pro/trader/triangular-arbitrage2-pro/node_modules/rxjs/src/internal/operators/tap.ts:120:21)\n' +
10 | ' at TapSubscriber.Subscriber.next (/Users/pro/trader/triangular-arbitrage2-pro/node_modules/rxjs/src/internal/Subscriber.ts:99:12)\n' +
11 | ' at MapSubscriber._next (/Users/pro/trader/triangular-arbitrage2-pro/node_modules/rxjs/src/internal/operators/map.ts:89:22)\n' +
12 | ' at MapSubscriber.Subscriber.next (/Users/pro/trader/triangular-arbitrage2-pro/node_modules/rxjs/src/internal/Subscriber.ts:99:12)\n' +
13 | ' at CatchSubscriber.Subscriber._next (/Users/pro/trader/triangular-arbitrage2-pro/node_modules/rxjs/src/internal/Subscriber.ts:139:22)\n' +
14 | ' at CatchSubscriber.Subscriber.next (/Users/pro/trader/triangular-arbitrage2-pro/node_modules/rxjs/src/internal/Subscriber.ts:99:12)\n' +
15 | ' at FilterSubscriber._next (/Users/pro/trader/triangular-arbitrage2-pro/node_modules/rxjs/src/internal/operators/filter.ts:101:24)\n' +
16 | ' at FilterSubscriber.Subscriber.next (/Users/pro/trader/triangular-arbitrage2-pro/node_modules/rxjs/src/internal/Subscriber.ts:99:12)';
17 | expect(getCallerMethodName(errStack)).toEqual('_tapNext');
18 | });
19 | it('getCallerMethodName #2', () => {
20 | const errStack =
21 | 'Error: \n' +
22 | ' at Logger.prototype. (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/src/app/common/descriptors/log-caller.decorator.ts:14:17)\n' +
23 | ' at prototype..execute (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/src/app/core/strategy/trading-strategy/multi-trading-strategy.ts:24:17)\n' +
24 | ' at prototype. (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/src/app/common/descriptors/catch-error.decorator.ts:15:41)\n' +
25 | ' at Strategy.execute (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/src/app/core/strategy/strategy.ts:13:33)\n' +
26 | ' at prototype..start (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/src/app/feature-modules/trade/trade.service.ts:49:25)\n' +
27 | ' at processTicksAndRejections (internal/process/task_queues.js:95:5)';
28 | expect(getCallerMethodName(errStack)).toEqual('execute');
29 | });
30 | it('getCallerMethodName #3', () => {
31 | const errStack =
32 | 'stack: Error\n' +
33 | ' at Logger.Object.getOwnPropertyNames.filter.forEach.e. (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/dist/triangular-arbitrage2-pro/bundle.js:40:366)\n' +
34 | ' at Object.getOwnPropertyNames.filter.forEach.o..start (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/dist/triangular-arbitrage2-pro/bundle.js:16:1113)\n' +
35 | ' at Object.getOwnPropertyNames.filter.forEach.o. (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/dist/triangular-arbitrage2-pro/bundle.js:24:383)\n' +
36 | ' at bootstrap (/Users/yuukisyaku/pro/trader/triangular-arbitrage2-pro/dist/triangular-arbitrage2-pro/bundle.js:276597:335)\n' +
37 | ' at processTicksAndRejections (internal/process/task_queues.js:95:5)\n';
38 | expect(getCallerMethodName(errStack)).toEqual('start');
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/libs/common/utils/get-caller-method-name/get-caller-method-name.ts:
--------------------------------------------------------------------------------
1 | export function getCallerMethodName(stack: string): string {
2 | const infoLines = stack.split('\n')[2].trim().replace('.', '').split('.');
3 | let mothedLine = infoLines[infoLines.length - 2];
4 | if (mothedLine === 'service') {
5 | mothedLine = infoLines[infoLines.length - 3];
6 | }
7 |
8 | return mothedLine.split(' ')[0];
9 | }
10 |
--------------------------------------------------------------------------------
/libs/common/utils/get-caller-method-name/index.ts:
--------------------------------------------------------------------------------
1 | export * from './get-caller-method-name';
2 |
--------------------------------------------------------------------------------
/libs/common/utils/get-edge-order-amount/get-edge-order-amount.spec.ts:
--------------------------------------------------------------------------------
1 | import { mockData } from '@ta2-libs/testing';
2 |
3 | import { Edge } from '../../../models';
4 | import { getEdgeOrderAmount } from './get-edge-order-amount';
5 |
6 | describe('getEdgeOrderAmount utils functions', () => {
7 | it('getEdgeOrderAmount #1', () => {
8 | const edge: Edge = {
9 | pair: 'BTC/BUSD',
10 | fromAsset: 'BUSD',
11 | toAsset: 'BTC',
12 | side: 'buy',
13 | price: 33457.48,
14 | quantity: 0.050511,
15 | };
16 | const pairInfo = mockData.pairInfo[edge.pair];
17 | const res = getEdgeOrderAmount(edge, pairInfo, 0);
18 | expect(res).toEqual(0.001195);
19 | });
20 | it('getEdgeOrderAmount #2', () => {
21 | const edge: Edge = { pair: 'ETH/BUSD', fromAsset: 'BUSD', toAsset: 'ETH', side: 'buy', price: 1978.33, quantity: 1.9686 };
22 | const pairInfo = mockData.pairInfo[edge.pair];
23 | const res = getEdgeOrderAmount(edge, pairInfo, 0);
24 | expect(res).toEqual(0.001195);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/libs/common/utils/get-edge-order-amount/get-edge-order-amount.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '@ta2-libs/config';
2 | import { Market } from 'ccxt';
3 |
4 | import { Edge } from '../../../models';
5 | import { add, divide, floor, multiple } from '../../big-number';
6 |
7 | /**
8 | * 获取单边下单数量
9 | * @param edge
10 | * @param pairInfo
11 | * @param fee
12 | */
13 | export function getEdgeOrderAmount(edge: Edge, pairInfo: Market, fee: number): number {
14 | const limits = pairInfo.limits;
15 | const precision = pairInfo.precision.amount;
16 | const minAmount = divide(limits.cost.min, edge.price);
17 | const attachedAmount = add(fee, multiple(minAmount, Config.root.orderTimes > 1 ? Config.root.orderTimes : 0.1));
18 |
19 | return floor(add(minAmount, attachedAmount), precision).toNumber();
20 | }
21 |
--------------------------------------------------------------------------------
/libs/common/utils/get-edge-order-amount/index.ts:
--------------------------------------------------------------------------------
1 | export * from './get-edge-order-amount';
2 |
--------------------------------------------------------------------------------
/libs/common/utils/get-triangle-rate/get-triangle-rate.spec.ts:
--------------------------------------------------------------------------------
1 | import { Edge } from '@ta2-libs/models';
2 |
3 | import { getTriangleRate } from './get-triangle-rate';
4 |
5 | describe('getTriangleRate utils functions', () => {
6 | it('getTriangleRate #1', () => {
7 | const a: Edge = {
8 | pair: 'ETHBTC',
9 | fromAsset: 'BTC',
10 | toAsset: 'ETH',
11 | side: 'buy',
12 | price: 0.072811,
13 | quantity: 21.763,
14 | };
15 | const b: Edge = {
16 | pair: 'EOSETH',
17 | fromAsset: 'ETH',
18 | toAsset: 'EOS',
19 | side: 'buy',
20 | price: 0.002324,
21 | quantity: 88.28,
22 | };
23 | const c: Edge = {
24 | pair: 'EOSBTC',
25 | fromAsset: 'EOS',
26 | toAsset: 'BTC',
27 | side: 'sell',
28 | price: 0.0001691,
29 | quantity: 523.99,
30 | };
31 | const res = getTriangleRate(a, b, c);
32 | expect(res).toEqual({
33 | rate: '-0.06664037',
34 | logs: {
35 | aRate: 'a rate = 1 / 0.072811 = 13.73418851 ETH',
36 | bRate: 'b rate = 13.73418851 / 0.002324 = 5909.71967102 EOS',
37 | cRate: 'c rate = (5909.71967102 x 0.0001691 -1) x 100 = -0.06664037%',
38 | },
39 | });
40 | });
41 |
42 | it('getTriangleRate #2', () => {
43 | const a: Edge = {
44 | pair: 'BUSD/USDT',
45 | fromAsset: 'BUSD',
46 | toAsset: 'USDT',
47 | side: 'sell',
48 | price: 0.9998,
49 | quantity: 13703885.49,
50 | };
51 | const b: Edge = {
52 | pair: 'BTC/USDT',
53 | fromAsset: 'USDT',
54 | toAsset: 'BTC',
55 | side: 'buy',
56 | price: 35097.01,
57 | quantity: 0.068097,
58 | };
59 | const c: Edge = {
60 | pair: 'BTC/BUSD',
61 | fromAsset: 'BTC',
62 | toAsset: 'BUSD',
63 | side: 'sell',
64 | price: 35105.65,
65 | quantity: 0.007184,
66 | };
67 | const res = getTriangleRate(a, b, c);
68 | expect(res).toEqual({
69 | rate: '0.02461748',
70 | logs: {
71 | aRate: '',
72 | bRate: 'b rate = 1 / 35097.01 = 0.00002849 BTC',
73 | cRate: 'c rate = (0.00002849 x 35105.65 -1) x 100 = 0.02461748%',
74 | },
75 | });
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/libs/common/utils/get-triangle-rate/get-triangle-rate.ts:
--------------------------------------------------------------------------------
1 | import { Edge, TriangleRate, TriangleRateLogs } from '../../../models';
2 | import { divide, floor, floorToString, getBigNumber, multiple, subtract } from '../../big-number';
3 |
4 | /**
5 | * 通过三边信息,获取合成交叉汇率
6 | * @param a
7 | * @param b
8 | * @param c
9 | */
10 | export function getTriangleRate(a: Edge, b: Edge, c: Edge): TriangleRate {
11 | // 利率 = (1/priceA/priceB*priceC-1)-1
12 | const capital = getBigNumber(1);
13 | let step1Rate = getBigNumber(1);
14 | const logInfos = {} as TriangleRateLogs;
15 | let unitAsset = a.fromAsset;
16 | if (a.side === 'buy') {
17 | unitAsset = a.toAsset;
18 | step1Rate = divide(capital, a.price);
19 | logInfos.aRate = `a rate = 1 / ${a.price} = ${floor(step1Rate, 8).toNumber()} ${unitAsset}`;
20 | } else {
21 | logInfos.aRate = '';
22 | }
23 | const fixedStep1Rate = floor(step1Rate, 8);
24 |
25 | let step2Rate = multiple(step1Rate, b.price);
26 | let operator = 'x';
27 | unitAsset = b.fromAsset;
28 | if (b.side === 'buy') {
29 | unitAsset = b.toAsset;
30 | step2Rate = divide(step1Rate, b.price);
31 | operator = '/';
32 | }
33 |
34 | const fixedStep2Rate = floor(step2Rate, 8);
35 | logInfos.bRate = `b rate = ${fixedStep1Rate} ${operator} ${b.price} = ${fixedStep2Rate.toNumber()} ${unitAsset}`;
36 |
37 | let step3Rate = multiple(step2Rate, c.price);
38 | operator = 'x';
39 | if (c.side === 'buy') {
40 | step3Rate = divide(step2Rate, c.price);
41 | operator = '/';
42 | }
43 | const fixedStep3Rate = floor(multiple(subtract(step3Rate, 1), 100), 8);
44 | logInfos.cRate = `c rate = (${fixedStep2Rate} ${operator} ${c.price} -1) x 100 = ${fixedStep3Rate.toNumber()}%`;
45 |
46 | return {
47 | rate: floorToString(fixedStep3Rate, 8),
48 | logs: logInfos,
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/libs/common/utils/get-triangle-rate/index.ts:
--------------------------------------------------------------------------------
1 | export * from './get-triangle-rate';
2 |
--------------------------------------------------------------------------------
/libs/common/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './get-triangle-rate';
2 | export * from './get-edge-order-amount';
3 | export * from './get-caller-method-name';
4 |
--------------------------------------------------------------------------------
/libs/config/config.ts:
--------------------------------------------------------------------------------
1 | import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions';
2 |
3 | export interface ConfigSettings {
4 | active: 'binance';
5 | orderTimes: number;
6 | processingOrderPatrolTime: number;
7 | sessionLimit: number;
8 | broker: {
9 | [broker: string]: ConfigBroker;
10 | };
11 | pro: ConfigPro;
12 | mysql: ConfigMysql;
13 | notification: ConfigNotification;
14 | connectionOptions: MysqlConnectionOptions;
15 | }
16 |
17 | export interface ConfigPro {
18 | strategy: string;
19 | }
20 |
21 | export interface ConfigBroker {
22 | profitRate: number;
23 | startAssets: string[];
24 | whitelist: string[];
25 | blacklist: string[];
26 | mode: 'real' | 'test';
27 | real: ConfigAPIKey;
28 | test: ConfigAPIKey;
29 | }
30 |
31 | export interface ConfigAPIKey {
32 | apiKey: string;
33 | secret: string;
34 | }
35 |
36 | export interface ConfigMysql {
37 | host: string;
38 | port: number;
39 | username: string;
40 | password: string;
41 | database: string;
42 | logging: boolean;
43 | }
44 |
45 | export interface ConfigNotification {
46 | email: ConfigEmail;
47 | }
48 |
49 | export interface ConfigEmail {
50 | enabled: boolean;
51 | smtpService: string;
52 | authUser: string;
53 | authPass: string;
54 | sendList: string[];
55 | }
56 |
57 | // tslint:disable-next-line:no-var-requires
58 | const root: ConfigSettings = require('config');
59 | const activeBroker = root.broker[root.active];
60 | const credential = activeBroker[activeBroker.mode];
61 | const pro = root.pro;
62 |
63 | const Config = {
64 | root,
65 | activeBroker,
66 | credential,
67 | pro,
68 | connectionOptions: {
69 | ...root.mysql,
70 | type: 'mysql',
71 | name: 'default',
72 | dropSchema: false,
73 | synchronize: false,
74 | migrationsRun: false,
75 | supportBigNumbers: true,
76 | bigNumberStrings: true,
77 | },
78 | };
79 |
80 | export { Config };
81 |
--------------------------------------------------------------------------------
/libs/config/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config';
2 |
--------------------------------------------------------------------------------
/libs/exceptions/exception-handler/default-exception-handler.spec.ts:
--------------------------------------------------------------------------------
1 | import { DefaultExceptionHandler } from './default-exception-handler';
2 |
3 | describe('DefaultExceptionFilter', () => {
4 | it('should handle exception', () => {
5 | const exception = new Error('test error');
6 | expect(DefaultExceptionHandler.handle('class', 'method', {}, exception));
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/libs/exceptions/exception-handler/default-exception-handler.ts:
--------------------------------------------------------------------------------
1 | import { ExceptionHandler } from './exception-handler';
2 |
3 | export class DefaultExceptionHandler extends ExceptionHandler {
4 | static handle(className: string, methodName: string, args: any, exception: Error): void {
5 | const _global = global as any;
6 | const logger = _global && _global.logger ? _global.logger : console;
7 | logger.error(`${className},${methodName}`, JSON.stringify(args), exception.stack);
8 | if (_global.notification) {
9 | const title = `${className}-${methodName}`;
10 | _global.notification.sendEmail({
11 | subject: `[异常警报] ${title}`,
12 | title,
13 | body: exception.stack,
14 | });
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/libs/exceptions/exception-handler/exception-handler.ts:
--------------------------------------------------------------------------------
1 | export class ExceptionHandler {
2 | handle(className: string, methodName: string, args: any, exception: Error): any {}
3 | }
4 |
--------------------------------------------------------------------------------
/libs/exceptions/exception-handler/index.ts:
--------------------------------------------------------------------------------
1 | export * from './default-exception-handler';
2 | export * from './exception-handler';
3 |
--------------------------------------------------------------------------------
/libs/exceptions/index.ts:
--------------------------------------------------------------------------------
1 | export * from './exception-handler';
2 |
--------------------------------------------------------------------------------
/libs/logger/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from './tokens';
2 | export * from './types';
3 | export * from './utils';
4 |
--------------------------------------------------------------------------------
/libs/logger/common/predef.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * data fields that may contain sensitive information
3 | */
4 | export const SENSITIVE_FIELDS_SET = new Set(['password']);
5 |
--------------------------------------------------------------------------------
/libs/logger/common/stringify.spec.ts:
--------------------------------------------------------------------------------
1 | import { stringify } from './stringify';
2 |
3 | describe('stringify', () => {
4 | it('primitive type', () => {
5 | expect(stringify(1)).toBe('1');
6 | expect(stringify(undefined)).toBe(undefined);
7 | expect(stringify([])).toBe('[]');
8 | expect(stringify({})).toBe('{}');
9 | expect(stringify({ a: 1 })).toEqual('{"a":1}');
10 | const t = new Date('2019-01-22T07:39:41.824Z');
11 | expect(stringify(t)).toBe('"2019-01-22T07:39:41.824Z"');
12 | });
13 |
14 | it('circular cases', () => {
15 | const obj: any = { a: 1 };
16 | obj.arr = obj;
17 |
18 | expect(() => {
19 | JSON.stringify(obj);
20 | }).toThrow();
21 |
22 | expect(stringify(obj)).toEqual('{"a":1,"arr":"~"}');
23 | });
24 |
25 | it('default dont replace sensitive fileds', () => {
26 | const sensitiveObj1 = {
27 | username: 'logger',
28 | password: '123456',
29 | };
30 | expect(stringify(sensitiveObj1)).toEqual('{"username":"logger","password":"123456"}');
31 | });
32 |
33 | it('sensitive fileds', () => {
34 | const sensitiveObj1 = {
35 | username: 'logger',
36 | password: '123456',
37 | };
38 | expect(stringify(sensitiveObj1, true)).toEqual('{"username":"logger","password":"*censored*"}');
39 | });
40 |
41 | it('sensitive fileds nested', () => {
42 | const sensitiveObj2 = {
43 | requestId: '1',
44 | user: {
45 | username: 'logger',
46 | password: '123456',
47 | },
48 | };
49 | expect(stringify(sensitiveObj2, true)).toEqual(`{"requestId":"1","user":{"username":"logger","password":"*censored*"}}`);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/libs/logger/common/stringify.ts:
--------------------------------------------------------------------------------
1 | import { stringify as circular_stringify } from 'circular-json';
2 |
3 | import { SENSITIVE_FIELDS_SET } from './predef';
4 |
5 | function replacer(key: string, value: T): T {
6 | if (SENSITIVE_FIELDS_SET.has(key)) {
7 | return ('*censored*' as any) as T;
8 | }
9 |
10 | return value;
11 | }
12 |
13 | export function stringify(data: any, replaceSensitive?: boolean): string {
14 | return replaceSensitive ? circular_stringify(data, replacer) : circular_stringify(data);
15 | }
16 |
--------------------------------------------------------------------------------
/libs/logger/common/tokens.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@ta2-libs/common';
2 |
3 | import { LogStrategy } from '../strategy';
4 | import { LogLevels } from './types';
5 |
6 | export const LOG_STRATEGY = new InjectionToken('LOG_STRATEGY');
7 | export const ENABLE_COLORS = new InjectionToken('ENABLE_COLORS');
8 | export const MIN_LOG_LEVEL = new InjectionToken('MIN_LOG_LEVEL');
9 | export const TAGS_EXCLUDE = new InjectionToken('TAGS_EXCLUDE');
10 | export const TAGS_INCLUDE = new InjectionToken('TAGS_INCLUDE');
11 |
--------------------------------------------------------------------------------
/libs/logger/common/types.ts:
--------------------------------------------------------------------------------
1 | export const LogLevels = {
2 | Debug: 'Debug',
3 | Log: 'Log',
4 | Info: 'Info',
5 | Warn: 'Warn',
6 | Error: 'Error',
7 | Event: 'Event',
8 | } as const;
9 |
10 | export type LogLevels = typeof LogLevels[keyof typeof LogLevels];
11 |
12 | export const LogLevelColors = {
13 | Debug: '#263238',
14 | Log: '#33691E',
15 | Info: '#01579B',
16 | Event: '#f601ff',
17 | Warn: '#BF360C',
18 | Error: '#B71C1C',
19 | } as const;
20 |
21 | export type LogLevelColors = typeof LogLevelColors[keyof typeof LogLevelColors];
22 |
23 | export const tagColor = '#d06748';
24 | export const dataColor = '#5abf2b';
25 |
--------------------------------------------------------------------------------
/libs/logger/common/utils/get-log-content.ts:
--------------------------------------------------------------------------------
1 | import { getTimestring } from '@ta2-libs/common';
2 |
3 | import { LogLevels } from '../types';
4 | import { makeColoredLogArgs } from './make-colored-log-args';
5 |
6 | export function getLogContent(enableColors: boolean, logLevel: LogLevels, tags: string[], ...data: unknown[]): string[] {
7 | let levelSpace = '';
8 | switch (logLevel) {
9 | case LogLevels.Log: {
10 | levelSpace = ' ';
11 | break;
12 | }
13 | case LogLevels.Info:
14 | case LogLevels.Warn: {
15 | levelSpace = ' ';
16 | break;
17 | }
18 | }
19 | let logArgs: any[];
20 | const tagStr = `[${tags.join('][')}]`;
21 | const dateLabel = ` - ${getTimestring()} `;
22 | if (enableColors) {
23 | logArgs = makeColoredLogArgs(logLevel, levelSpace, tagStr, dateLabel, ...data);
24 | } else {
25 | logArgs = [`[${logLevel}${levelSpace}]${tagStr} `, dateLabel, ...data];
26 | }
27 |
28 | return logArgs;
29 | }
30 |
--------------------------------------------------------------------------------
/libs/logger/common/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './get-log-content';
2 | export * from './is-allowed-log-level';
3 | export * from './make-colored-log-args';
4 |
--------------------------------------------------------------------------------
/libs/logger/common/utils/is-allowed-log-level.ts:
--------------------------------------------------------------------------------
1 | import { LogLevels } from '../types';
2 |
3 | export function isAllowedLogLevel(minLogLevel: LogLevels, target: LogLevels): boolean {
4 | const logLevelStrength: Record = {
5 | Debug: 1,
6 | Log: 2,
7 | Info: 3,
8 | Event: 4,
9 | Warn: 5,
10 | Error: 6,
11 | };
12 | return logLevelStrength[target] >= logLevelStrength[minLogLevel];
13 | }
14 |
--------------------------------------------------------------------------------
/libs/logger/common/utils/make-colored-log-args.ts:
--------------------------------------------------------------------------------
1 | import * as chalk from 'chalk';
2 |
3 | import { LogLevelColors, LogLevels, dataColor, tagColor } from '../types';
4 |
5 | function colorLevel(logLevel: LogLevels, levelSpace: string): string {
6 | return `${chalk.whiteBright.bgHex(LogLevelColors[logLevel]).bold(` ${logLevel}${levelSpace} `)}${chalk
7 | .hex(LogLevelColors[logLevel])
8 | .bgBlack('')}`;
9 | }
10 |
11 | export function makeColoredLogArgs(logLevel: LogLevels, levelSpace: string, tag: string, dateLabel: string, ...data: unknown[]): unknown[] {
12 | const loggerColorSettings: Record = {
13 | Debug: colorLevel(LogLevels.Debug, levelSpace),
14 | Log: colorLevel(LogLevels.Log, levelSpace),
15 | Info: colorLevel(LogLevels.Info, levelSpace),
16 | Event: colorLevel(LogLevels.Event, levelSpace),
17 | Warn: colorLevel(LogLevels.Warn, levelSpace),
18 | Error: colorLevel(LogLevels.Error, levelSpace),
19 | };
20 |
21 | return [loggerColorSettings[logLevel], dateLabel, chalk.hex(tagColor).bold(`${tag} `), chalk.hex(dataColor).bold(...data)];
22 | }
23 |
--------------------------------------------------------------------------------
/libs/logger/index.ts:
--------------------------------------------------------------------------------
1 | export * from './logger.module';
2 | export * from './logger';
3 | export * from './common';
4 | export * from './strategy';
5 |
--------------------------------------------------------------------------------
/libs/logger/logger.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Global, Module, Type } from '@nestjs/common';
2 |
3 | import { ENABLE_COLORS, LOG_STRATEGY, MIN_LOG_LEVEL, TAGS_EXCLUDE, TAGS_INCLUDE } from './common/tokens';
4 | import { LogLevels } from './common/types';
5 | import { Logger } from './logger';
6 | import { ConsoleLogStrategy, LogStrategy } from './strategy';
7 |
8 | interface LoggerConfig {
9 | strategy?: Type;
10 | minLogLevel?: LogLevels;
11 | enableColors?: boolean;
12 | tagsExclude?: string[];
13 | tagsInclude?: string[];
14 | }
15 |
16 | @Global()
17 | @Module({
18 | providers: [Logger],
19 | exports: [Logger],
20 | })
21 | export class LoggerModule {
22 | static forRoot(config: LoggerConfig): DynamicModule {
23 | return {
24 | module: LoggerModule,
25 | providers: [
26 | {
27 | provide: LOG_STRATEGY as any,
28 | useClass: config.strategy || ConsoleLogStrategy,
29 | },
30 | {
31 | provide: MIN_LOG_LEVEL as any,
32 | useValue: (config.minLogLevel || LogLevels.Debug) as LogLevels,
33 | },
34 | {
35 | provide: ENABLE_COLORS as any,
36 | useValue: config.enableColors,
37 | },
38 | {
39 | provide: TAGS_EXCLUDE as any,
40 | useValue: config.tagsExclude || [],
41 | },
42 | {
43 | provide: TAGS_INCLUDE,
44 | useValue: config.tagsInclude || [],
45 | },
46 | Logger,
47 | ],
48 | };
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/libs/logger/logger.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 |
3 | import { Logger } from './logger';
4 |
5 | describe('Logger', () => {
6 | let service: Logger;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | providers: [Logger],
11 | }).compile();
12 |
13 | service = module.get(Logger);
14 | });
15 |
16 | it('should be defined', () => {
17 | expect(service).toBeDefined();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/libs/logger/logger.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Logger as NestLogger } from '@nestjs/common';
2 | import { LogCaller } from '@ta2-libs/common';
3 |
4 | import { LOG_STRATEGY, MIN_LOG_LEVEL, TAGS_EXCLUDE, TAGS_INCLUDE } from './common/tokens';
5 | import { LogLevels } from './common/types';
6 | import { isAllowedLogLevel } from './common/utils';
7 | import { LogStrategy } from './strategy/log-strategy';
8 |
9 | @LogCaller()
10 | @Injectable()
11 | export class Logger extends NestLogger {
12 | constructor(
13 | @Inject(LOG_STRATEGY) private readonly strategy: LogStrategy,
14 | @Inject(MIN_LOG_LEVEL) private readonly minLogLevel: LogLevels,
15 | @Inject(TAGS_EXCLUDE) private readonly tagsExclude: string[],
16 | @Inject(TAGS_INCLUDE) private readonly tagsInclude: string[],
17 | ) {
18 | super();
19 | }
20 |
21 | debug(tag: string, ...data: unknown[]): void {
22 | this.requestLog(LogLevels.Debug, tag, ...data);
23 | }
24 |
25 | log(tag: string, ...data: unknown[]): void {
26 | this.requestLog(LogLevels.Log, tag, ...data);
27 | }
28 |
29 | info(tag: string, ...data: unknown[]): void {
30 | this.requestLog(LogLevels.Info, tag, ...data);
31 | }
32 |
33 | warn(tag: string, ...data: unknown[]): void {
34 | this.requestLog(LogLevels.Warn, tag, ...data);
35 | }
36 |
37 | error(tag: string, ...data: unknown[]): void {
38 | this.requestLog(LogLevels.Error, tag, ...data);
39 | }
40 |
41 | event(tag: string, ...data: unknown[]): void {
42 | this.requestLog(LogLevels.Event, tag, ...data);
43 | }
44 |
45 | private requestLog(logLevel: LogLevels, tag: string, ...data: unknown[]): void {
46 | if (!isAllowedLogLevel(this.minLogLevel, logLevel)) {
47 | return; // skip
48 | }
49 |
50 | const tags = tag.split(',');
51 | for (const excluded of this.tagsExclude) {
52 | if (tags.includes(excluded)) {
53 | return; // skip
54 | }
55 | }
56 | for (const included of this.tagsInclude) {
57 | if (!tags.includes(included)) {
58 | return; // skip
59 | }
60 | }
61 | this.strategy.log(logLevel, tags, ...data);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/libs/logger/strategy/console-log-strategy.spec.ts:
--------------------------------------------------------------------------------
1 | import { LogLevels } from '@ta2-libs/logger';
2 |
3 | import { ConsoleLogStrategy } from './console-log-strategy';
4 |
5 | describe('ConsoleLogStrategy', () => {
6 | describe('logLevel = debug', () => {
7 | it('should call console.debug() [no color]', () => {
8 | jest.spyOn(console, 'debug');
9 | const strategy = new ConsoleLogStrategy(false);
10 |
11 | strategy.log(LogLevels.Debug, ['test'], 'foobar', 1);
12 | // eslint-disable-next-line no-console
13 | expect(console.debug).toHaveBeenCalledWith(`[test]`, `foobar`, 1);
14 | });
15 |
16 | it('should call console.debug() [color]', () => {
17 | jest.spyOn(console, 'debug');
18 | const strategy = new ConsoleLogStrategy(true);
19 |
20 | strategy.log(LogLevels.Debug, ['test'], 'foobar', 1);
21 | // eslint-disable-next-line no-console
22 | expect(console.debug).toHaveBeenCalledWith(`%c debug:`, `background: #263238; color: #ffffff`, `[test]`, `foobar`, 1);
23 | });
24 | });
25 |
26 | describe('logLevel = log', () => {
27 | it('should call console.log() [no color]', () => {
28 | jest.spyOn(console, 'log');
29 | const strategy = new ConsoleLogStrategy(false);
30 |
31 | strategy.log(LogLevels.Log, ['test'], 'foobar', 1);
32 | // eslint-disable-next-line no-console
33 | expect(console.log).toHaveBeenCalledWith(`[test]`, `foobar`, 1);
34 | });
35 |
36 | it('should call console.log() [color]', () => {
37 | jest.spyOn(console, 'log');
38 | const strategy = new ConsoleLogStrategy(true);
39 |
40 | strategy.log(LogLevels.Log, ['test'], 'foobar', 1);
41 | // eslint-disable-next-line no-console
42 | expect(console.log).toHaveBeenCalledWith('%c log:', 'background: #33691E; color: #ffffff', `[test]`, `foobar`, 1);
43 | });
44 | });
45 |
46 | describe('logLevel = info', () => {
47 | it('should call console.info() [no color]', () => {
48 | jest.spyOn(console, 'info');
49 | const strategy = new ConsoleLogStrategy(false);
50 |
51 | strategy.log(LogLevels.Info, ['test'], 'foobar', 1);
52 | // eslint-disable-next-line no-console
53 | expect(console.info).toHaveBeenCalledWith(`[test]`, `foobar`, 1);
54 | });
55 |
56 | it('should call console.info() [color]', () => {
57 | jest.spyOn(console, 'info');
58 | const strategy = new ConsoleLogStrategy(true);
59 |
60 | strategy.log(LogLevels.Info, ['test'], 'foobar', 1);
61 | // eslint-disable-next-line no-console
62 | expect(console.info).toHaveBeenCalledWith('%c info:', 'background: #01579B; color: #ffffff', `[test]`, `foobar`, 1);
63 | });
64 | });
65 |
66 | describe('logLevel = warn', () => {
67 | it('should call console.warn() [no color]', () => {
68 | jest.spyOn(console, 'warn');
69 | const strategy = new ConsoleLogStrategy(false);
70 |
71 | strategy.log(LogLevels.Warn, ['test'], 'foobar', 1);
72 | // eslint-disable-next-line no-console
73 | expect(console.warn).toHaveBeenCalledWith(`[test]`, `foobar`, 1);
74 | });
75 |
76 | it('should call console.warn() [color]', () => {
77 | jest.spyOn(console, 'warn');
78 | const strategy = new ConsoleLogStrategy(true);
79 |
80 | strategy.log(LogLevels.Warn, ['test'], 'foobar', 1);
81 | // eslint-disable-next-line no-console
82 | expect(console.warn).toHaveBeenCalledWith('%c warn:', 'background: #BF360C; color: #ffffff', `[test]`, `foobar`, 1);
83 | });
84 | });
85 |
86 | describe('logLevel = error', () => {
87 | it('should call console.error() [no color]', () => {
88 | jest.spyOn(console, 'error');
89 | const strategy = new ConsoleLogStrategy(false);
90 |
91 | strategy.log(LogLevels.Error, ['test'], 'foobar', 1);
92 | // eslint-disable-next-line no-console
93 | expect(console.error).toHaveBeenCalledWith(`[test]`, `foobar`, 1);
94 | });
95 |
96 | it('should call console.error() [color]', () => {
97 | jest.spyOn(console, 'error');
98 | const strategy = new ConsoleLogStrategy(true);
99 |
100 | strategy.log(LogLevels.Error, ['test'], 'foobar', 1);
101 | // eslint-disable-next-line no-console
102 | expect(console.error).toHaveBeenCalledWith('%c error:', 'background: #B71C1C; color: #ffffff', `[test]`, `foobar`, 1);
103 | });
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/libs/logger/strategy/console-log-strategy.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@nestjs/common';
2 |
3 | import { ENABLE_COLORS, LogLevels, getLogContent } from '../common';
4 | import { LogStrategy } from '../strategy';
5 |
6 | @Injectable()
7 | export class ConsoleLogStrategy implements LogStrategy {
8 | constructor(@Inject(ENABLE_COLORS) private readonly enableColors: boolean) {}
9 | log(logLevel: LogLevels, tags: string[], ...data: unknown[]): void {
10 | const logArgs = getLogContent(this.enableColors, logLevel, tags, ...data);
11 |
12 | switch (logLevel) {
13 | case LogLevels.Debug: {
14 | // eslint-disable-next-line no-console
15 | console.debug(...logArgs);
16 | return;
17 | }
18 | case LogLevels.Log: {
19 | console.log(...logArgs);
20 | return;
21 | }
22 | case LogLevels.Info: {
23 | console.info(...logArgs);
24 | return;
25 | }
26 | case LogLevels.Warn: {
27 | // eslint-disable-next-line no-console
28 | console.warn(...logArgs);
29 | return;
30 | }
31 | case LogLevels.Error: {
32 | console.error(...logArgs);
33 | return;
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/libs/logger/strategy/index.ts:
--------------------------------------------------------------------------------
1 | export * from './console-log-strategy';
2 | export * from './multi-log-strategy';
3 | export * from './log-strategy';
4 |
--------------------------------------------------------------------------------
/libs/logger/strategy/log-strategy.ts:
--------------------------------------------------------------------------------
1 | import { LogLevels } from '../common';
2 |
3 | export interface LogStrategy {
4 | log(logLevel: LogLevels, tags: string[], ...data: unknown[]): void;
5 | }
6 |
7 | export class NoopLogStrategy implements LogStrategy {
8 | log(): void {}
9 | }
10 |
--------------------------------------------------------------------------------
/libs/logger/strategy/multi-log-strategy.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@nestjs/common';
2 | import { Logger, configure, getLogger } from 'log4js';
3 |
4 | import { ENABLE_COLORS, LogLevels, getLogContent } from '../common';
5 | import { LogStrategy } from './log-strategy';
6 |
7 | export const LogCategory = {
8 | App: 'app',
9 | Debug: 'debug',
10 | Error: 'error',
11 | Event: 'event',
12 | } as const;
13 |
14 | export type LogCategory = typeof LogCategory[keyof typeof LogCategory];
15 |
16 | const defaultAppender = {
17 | type: 'multiFile',
18 | pattern: '.yyyyMMdd',
19 | maxLogSize: 1024 * 1024 * 50,
20 | backups: 300,
21 | compress: true,
22 | keepFileExt: true,
23 | layout: {
24 | type: 'messagePassThrough',
25 | },
26 | };
27 |
28 | @Injectable()
29 | export class MultiLogStrategy implements LogStrategy {
30 | private loggerMap: Map = new Map();
31 |
32 | constructor(@Inject(ENABLE_COLORS) private readonly enableColors: boolean) {
33 | configure({
34 | appenders: {
35 | multi: { ...defaultAppender, base: 'logs/', property: 'categoryName', extension: '.log' },
36 | console: { type: 'console', layout: { type: 'messagePassThrough' } },
37 | },
38 | categories: {
39 | default: { appenders: ['multi'], level: 'ALL' },
40 | console: { appenders: ['console'], level: 'ALL' },
41 | },
42 | });
43 |
44 | this.loggerMap.set(LogCategory.App, getLogger(LogCategory.App));
45 | this.loggerMap.set(LogCategory.Debug, getLogger(LogCategory.Debug));
46 | this.loggerMap.set(LogCategory.Error, getLogger(LogCategory.Error));
47 | this.loggerMap.set(LogCategory.Event, getLogger(LogCategory.Event));
48 | }
49 |
50 | log(logLevel: LogLevels, tags: string[], ...data: unknown[]): void {
51 | const colorMessage = getLogContent(this.enableColors, logLevel, tags, ...data).join('');
52 | const message = getLogContent(false, logLevel, tags, ...data).join('');
53 | switch (logLevel) {
54 | case LogLevels.Warn:
55 | case LogLevels.Debug: {
56 | this.loggerMap.get(LogCategory.Debug).debug(message);
57 | getLogger('console').debug(colorMessage);
58 | return;
59 | }
60 | case LogLevels.Log:
61 | case LogLevels.Info: {
62 | this.loggerMap.get(LogCategory.App).info(message);
63 | getLogger('console').info(colorMessage);
64 | return;
65 | }
66 | case LogLevels.Error: {
67 | this.loggerMap.get(LogCategory.Error).error(message);
68 | getLogger('console').error(colorMessage);
69 | return;
70 | }
71 | case LogLevels.Event: {
72 | this.loggerMap.get(LogCategory.Event).info(message);
73 | getLogger('console').info(colorMessage);
74 | return;
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/libs/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './trade';
2 | export * from './strategy';
3 |
--------------------------------------------------------------------------------
/libs/models/strategy/index.ts:
--------------------------------------------------------------------------------
1 | export * from './strategy';
2 | export * from './tokens';
3 | export * from './trading-strategy';
4 |
--------------------------------------------------------------------------------
/libs/models/strategy/strategy.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@nestjs/common';
2 | import { Observable } from 'rxjs';
3 |
4 | import { TradeTriangle } from '../../models';
5 | import { TRADING_STRATEGY } from './tokens';
6 | import { TradingStrategy } from './trading-strategy';
7 |
8 | @Injectable()
9 | export class Strategy {
10 | constructor(@Inject(TRADING_STRATEGY) private readonly tradingStrategy: TradingStrategy) {}
11 |
12 | execute(triangle: TradeTriangle): Promise {
13 | return this.tradingStrategy.execute(triangle);
14 | }
15 |
16 | getResult$(): Observable {
17 | return this.tradingStrategy.getResult$();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/libs/models/strategy/tokens.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@ta2-libs/common';
2 |
3 | import { TradingStrategy } from './trading-strategy';
4 |
5 | export const TRADING_STRATEGY = new InjectionToken('TRADING_STRATEGY');
6 |
--------------------------------------------------------------------------------
/libs/models/strategy/trading-strategy.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 |
3 | import { TradeTriangle } from '../trade';
4 |
5 | export interface TradingStrategy {
6 | execute(triangle: TradeTriangle): Promise;
7 | getResult$(): Observable;
8 | }
9 |
10 | export class NoopTradingStrategy implements TradingStrategy {
11 | execute(): Promise {
12 | return;
13 | }
14 |
15 | getResult$(): Observable {
16 | return;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/libs/models/trade.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 交易状态
3 | */
4 | export const TradeStatus = {
5 | Open: 'open',
6 | Closed: 'closed',
7 | Canceled: 'canceled',
8 | Todo: 'todo',
9 | } as const;
10 |
11 | export type TradeStatus = typeof TradeStatus[keyof typeof TradeStatus];
12 |
13 | /**
14 | * 用户资产信息
15 | */
16 | export interface UserAsset {
17 | // 资产名称
18 | asset: string;
19 | // 可用余额
20 | free: string;
21 | // 冻结余额
22 | locked: string;
23 | }
24 |
25 | /**
26 | * 三角组合的边
27 | */
28 | export interface Edge {
29 | pair: string;
30 | fromAsset: string;
31 | toAsset: string;
32 | // 交易方向
33 | side: 'sell' | 'buy';
34 | // 最佳价格
35 | price: number;
36 | // 最佳数量
37 | quantity: number;
38 | }
39 |
40 | export interface TradeEdge extends Edge {
41 | id: string;
42 | fee: number;
43 | status: TradeStatus;
44 | }
45 |
46 | /**
47 | * 三角组合
48 | */
49 | export interface Triangle {
50 | // 三角组合唯一id(例:btc-bnb-bcd)
51 | id: string;
52 | edges: Edge[];
53 | // 利率
54 | rate: string;
55 | // 时间戳
56 | time: number;
57 | logs: TriangleRateLogs;
58 | }
59 |
60 | /**
61 | * 可交易三角组合
62 | */
63 | export interface TradeTriangle extends Triangle {
64 | edges: TradeEdge[];
65 | status: TradeStatus;
66 | openTime: number;
67 | }
68 |
69 | export interface ABCAssetName {
70 | aAssetName: string;
71 | bAssetName: string;
72 | cAssetName: string;
73 | }
74 |
75 | export interface TriangleRate {
76 | rate: string;
77 | logs: TriangleRateLogs;
78 | }
79 |
80 | export interface TriangleRateLogs {
81 | aRate: string;
82 | bRate: string;
83 | cRate: string;
84 | }
85 |
86 | /**
87 | * timestamp
88 | * eg: 1535600337261
89 | */
90 | export type Timestamp = Nominal;
91 |
92 | export type Nominal = T & {
93 | [Symbol.species]: Name;
94 | };
95 |
--------------------------------------------------------------------------------
/libs/modules/data/data.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 |
3 | import { DataService } from './data.service';
4 |
5 | @Global()
6 | @Module({
7 | providers: [DataService],
8 | exports: [DataService],
9 | })
10 | export class DataModule {}
11 |
--------------------------------------------------------------------------------
/libs/modules/data/data.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { BinanceApiModule, BinanceApiService } from '@ta2-libs/broker-api';
3 | import { useFirstValue } from '@ta2-libs/common';
4 | import { MockModule, buildDeferInitService } from '@ta2-libs/testing';
5 |
6 | import { DataService } from './data.service';
7 |
8 | describe('DataService', () => {
9 | let service: DataService;
10 |
11 | beforeEach(async () => {
12 | const module: TestingModule = await Test.createTestingModule({
13 | imports: [MockModule, BinanceApiModule],
14 | providers: [DataService],
15 | }).compile();
16 |
17 | await buildDeferInitService(module.get(BinanceApiService));
18 | service = await buildDeferInitService(module.get(DataService));
19 | });
20 |
21 | it('should be defined', () => {
22 | expect(service).toBeDefined();
23 | });
24 |
25 | it('getTickers$', async () => {
26 | const tickers = await useFirstValue(service.getTickers$());
27 | expect(Object.keys(tickers).length).toBeGreaterThan(10);
28 | expect(tickers['ETHBTC']).toBeDefined();
29 | });
30 |
31 | it('getTicker$', async () => {
32 | expect(await useFirstValue(service.getTicker$('ETHBTC'))).toBeDefined();
33 | expect(await useFirstValue(service.getTicker$('ethbtc'))).toBeDefined();
34 | });
35 |
36 | it('onCustomFilled$', async (done) => {
37 | const data = { t: 1 } as any;
38 | service.onCustomFilled$().subscribe((o) => {
39 | expect(o).toEqual(data);
40 | done();
41 | });
42 | service.customFilled$.next(data);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/libs/modules/data/data.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, OnModuleInit } from '@nestjs/common';
2 | import { AssetMarkets, BinanceApiService, EventType, ExecutionType, PairFees, Ticker24Hr, Tickers, UserData } from '@ta2-libs/broker-api';
3 | import { UserAsset } from '@ta2-libs/models';
4 | import { Balances, Market, Order } from 'ccxt';
5 | import { Observable, Subject } from 'rxjs';
6 | import { filter, map } from 'rxjs/operators';
7 |
8 | import { CatchError } from '../../common/descriptors';
9 | import { DefaultExceptionHandler } from '../../exceptions';
10 |
11 | @Injectable()
12 | @CatchError(DefaultExceptionHandler)
13 | export class DataService implements OnModuleInit {
14 | private rest = this.binanceApi.rest;
15 | private websocket = this.binanceApi.ws;
16 | private tickers$ = new Subject();
17 | private userAssets$ = new Subject();
18 | private _assetMarkets: AssetMarkets;
19 | private _pairFees: PairFees;
20 |
21 | userData$ = new Subject();
22 | customFilled$ = new Subject();
23 | tickers: Tickers;
24 | userAssets: UserAsset[] = [];
25 | onReady = this.binanceApi.onReady;
26 |
27 | get assetMarkets(): AssetMarkets {
28 | return this._assetMarkets;
29 | }
30 |
31 | get pairFees(): PairFees {
32 | return this._pairFees;
33 | }
34 |
35 | constructor(private binanceApi: BinanceApiService) {}
36 |
37 | onModuleInit(): void {
38 | this.onReady.pipe(filter((isReady) => isReady)).subscribe(() => this.init());
39 | }
40 |
41 | onCustomFilled$(): Observable {
42 | return this.customFilled$.asObservable();
43 | }
44 |
45 | onUserAssets$(): Observable {
46 | return this.userAssets$.asObservable();
47 | }
48 |
49 | onOrderFilled$(): Observable {
50 | return this.userData$
51 | .asObservable()
52 | .pipe(filter((data) => data.eventType === EventType.ExecutionReport && data.orderStatus === ExecutionType.FILLED));
53 | }
54 |
55 | getTicker$(pair: string): Observable {
56 | return this.tickers$.asObservable().pipe(map((tickers) => tickers[pair.toUpperCase()]));
57 | }
58 |
59 | getTickers$(): Observable {
60 | return this.tickers$.asObservable();
61 | }
62 |
63 | getBalance(): Promise {
64 | return this.rest.getBalance();
65 | }
66 |
67 | getPairInfo(pairName: string): Market | undefined {
68 | return this.rest.getPairInfo(pairName);
69 | }
70 |
71 | private async init(): Promise {
72 | this._assetMarkets = this.rest.assetMarkets;
73 | this._pairFees = this.rest.pairFees;
74 | await this.initUserAssets();
75 | this.websocket.getAllTickers$().subscribe((tickers) => {
76 | this.tickers$.next(tickers);
77 | this.tickers = tickers;
78 | });
79 | this.websocket.getUserData$().subscribe((userData) => {
80 | if (userData.eventType === EventType.OutboundAccountPosition) {
81 | for (const balance of userData.balances) {
82 | const useAsset = this.userAssets.find((o) => o.asset === balance.asset);
83 | if (useAsset) {
84 | useAsset.free = balance.availableBalance;
85 | useAsset.locked = balance.onOrderBalance;
86 | } else {
87 | this.userAssets.push({
88 | asset: balance.asset,
89 | free: balance.availableBalance,
90 | locked: balance.onOrderBalance,
91 | });
92 | }
93 | }
94 | this.userAssets$.next(this.userAssets);
95 | }
96 |
97 | return this.userData$.next(userData);
98 | });
99 | }
100 |
101 | private async initUserAssets(): Promise {
102 | const balance = await this.rest.getBalance();
103 | if (balance && balance.info && balance.info.balances) {
104 | this.userAssets = balance.info.balances
105 | .filter((o) => +o.free > 0 || +o.locked > 0)
106 | .map((balance) => ({
107 | asset: balance.asset,
108 | free: balance.free,
109 | locked: balance.locked,
110 | }));
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/libs/modules/data/index.ts:
--------------------------------------------------------------------------------
1 | export * from './data.service';
2 | export * from './data.module';
3 |
--------------------------------------------------------------------------------
/libs/modules/engine/engine.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 |
3 | import { EngineService } from './engine.service';
4 |
5 | @Module({
6 | providers: [EngineService],
7 | exports: [EngineService],
8 | })
9 | export class EngineModule {}
10 |
--------------------------------------------------------------------------------
/libs/modules/engine/engine.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { BinanceApiService } from '@ta2-libs/broker-api';
3 | import { useFirstValue } from '@ta2-libs/common';
4 | import { MockModule, buildDeferInitService } from '@ta2-libs/testing';
5 |
6 | import { DataModule, DataService } from '../data';
7 | import { EngineService } from './engine.service';
8 |
9 | describe('EngineService', () => {
10 | let service: EngineService;
11 |
12 | beforeEach(async () => {
13 | const module: TestingModule = await Test.createTestingModule({
14 | imports: [MockModule, DataModule],
15 | providers: [EngineService],
16 | }).compile();
17 |
18 | await buildDeferInitService(module.get(BinanceApiService));
19 | await buildDeferInitService(module.get(DataService));
20 | service = module.get(EngineService);
21 | });
22 |
23 | it('should be defined', () => {
24 | expect(service).toBeDefined();
25 | });
26 |
27 | it('should be get candidates', async () => {
28 | const candidates = await useFirstValue(service.getCandidates$());
29 | expect(candidates.length).toEqual(20);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/libs/modules/engine/index.ts:
--------------------------------------------------------------------------------
1 | export * from './engine.service';
2 | export * from './engine.module';
3 |
--------------------------------------------------------------------------------
/libs/modules/index.ts:
--------------------------------------------------------------------------------
1 | export * from './trade';
2 | export * from './data';
3 | export * from './engine';
4 | export * from './shared';
5 |
--------------------------------------------------------------------------------
/libs/modules/shared/index.ts:
--------------------------------------------------------------------------------
1 | export * from './on-destroy';
2 | export * from './shared.module';
3 |
--------------------------------------------------------------------------------
/libs/modules/shared/on-destroy/index.ts:
--------------------------------------------------------------------------------
1 | export * from './on-destroy.service';
2 |
--------------------------------------------------------------------------------
/libs/modules/shared/on-destroy/on-destroy.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 |
3 | import { OnDestroyService } from './on-destroy.service';
4 |
5 | describe('OnDestroyService', () => {
6 | let service: OnDestroyService;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | providers: [OnDestroyService],
11 | }).compile();
12 |
13 | service = module.get(OnDestroyService);
14 | });
15 |
16 | it('should be defined', () => {
17 | expect(service).toBeDefined();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/libs/modules/shared/on-destroy/on-destroy.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, OnModuleDestroy } from '@nestjs/common';
2 | import { Subject } from 'rxjs';
3 |
4 | @Injectable()
5 | export class OnDestroyService implements OnModuleDestroy {
6 | // control subscription of observables.
7 | // when emit true, all subscribe on this component are disposed.
8 | protected onDestroy$ = new Subject();
9 |
10 | constructor() {}
11 |
12 | onModuleDestroy(): void {
13 | this.onDestroy$.next(true);
14 | this.onDestroy$.complete();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/libs/modules/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 |
3 | import { OnDestroyService } from './on-destroy';
4 |
5 | @Module({
6 | imports: [OnDestroyService],
7 | exports: [OnDestroyService],
8 | })
9 | export class SharedModule {}
10 |
--------------------------------------------------------------------------------
/libs/modules/trade/index.ts:
--------------------------------------------------------------------------------
1 | export * from './trade.service';
2 | export * from './trade.module';
3 |
--------------------------------------------------------------------------------
/libs/modules/trade/trade.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 |
3 | import { TradeService } from './trade.service';
4 |
5 | @Module({
6 | providers: [TradeService],
7 | exports: [TradeService],
8 | })
9 | export class TradeModule {}
10 |
--------------------------------------------------------------------------------
/libs/notifications/email/email-handle.spec.ts:
--------------------------------------------------------------------------------
1 | import { EmailHandle } from './email-handle';
2 |
3 | describe('EmailHandle', () => {
4 | let handle: EmailHandle;
5 |
6 | beforeEach(async () => {
7 | handle = new EmailHandle();
8 | });
9 |
10 | it('should be send email', async () => {
11 | await handle.send({
12 | subject: '[テスト] nodemailer test mail---a',
13 | title: 'testTitle',
14 | body: 'zzzzzz',
15 | });
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/libs/notifications/email/email-handle.ts:
--------------------------------------------------------------------------------
1 | import { getTimestring } from '@ta2-libs/common';
2 | import { Config } from '@ta2-libs/config';
3 | import { SendMailOptions, createTransport } from 'nodemailer';
4 |
5 | import { emailTemplate } from './email.template';
6 |
7 | export type SendMailInputs = Pick & {
8 | title: string;
9 | body: string;
10 | };
11 |
12 | const config = Config.root.notification.email;
13 |
14 | export class EmailHandle {
15 | async send(inputs: SendMailInputs): Promise {
16 | if (!config.enabled) {
17 | return;
18 | }
19 |
20 | const smtp = createTransport({
21 | service: config.smtpService,
22 | secure: true,
23 | auth: {
24 | user: config.authUser,
25 | pass: config.authPass,
26 | },
27 | });
28 |
29 | // メール情報の作成
30 | const message: SendMailOptions = {
31 | from: `"triangular-arbitrage2"<${config.authUser}`,
32 | to: config.sendList,
33 | subject: inputs.subject,
34 | html: emailTemplate(inputs.title, `时间: ${inputs.body}
${getTimestring()}`),
35 | attachments: [
36 | {
37 | filename: 'logo.png',
38 | path: 'assets/images/logo_circle.png',
39 | cid: 'logo',
40 | },
41 | ],
42 | };
43 |
44 | await smtp.sendMail(message);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/libs/notifications/email/email.template.ts:
--------------------------------------------------------------------------------
1 | export const emailTemplate = (title: string, body: string) => `
2 |
4 |
5 |
6 | ${title}
7 |
8 |
9 |
47 |
48 |
49 |
50 |
51 |
52 |
${title}
53 |
${body}
54 |
58 |
59 |
60 |
61 |
62 | `;
63 |
--------------------------------------------------------------------------------
/libs/notifications/email/index.ts:
--------------------------------------------------------------------------------
1 | export * from './email-handle';
2 |
--------------------------------------------------------------------------------
/libs/notifications/index.ts:
--------------------------------------------------------------------------------
1 | export * from './notification-manager';
2 | export * from './email';
3 |
--------------------------------------------------------------------------------
/libs/notifications/notification-manager.spec.ts:
--------------------------------------------------------------------------------
1 | import { NotificationManager } from './notification-manager';
2 |
3 | describe('EmailHandle', () => {
4 | let manger: NotificationManager;
5 |
6 | beforeEach(async () => {
7 | manger = new NotificationManager();
8 | });
9 |
10 | it('should be send email', async () => {
11 | await manger.sendEmail({
12 | subject: '[テスト] nodemailer test mail---a',
13 | title: 'testTitle',
14 | body: 'zzzzzz',
15 | });
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/libs/notifications/notification-manager.ts:
--------------------------------------------------------------------------------
1 | import { EmailHandle, SendMailInputs } from './email';
2 |
3 | export class NotificationManager {
4 | private readonly email: EmailHandle;
5 |
6 | constructor() {
7 | this.email = new EmailHandle();
8 | }
9 |
10 | public sendEmail(inputs: SendMailInputs): Promise {
11 | return this.email.send(inputs);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/libs/persistence/common/constants.ts:
--------------------------------------------------------------------------------
1 | import { ObjectType } from 'typeorm';
2 |
3 | import { TradeEdgeEntity, TradeEdgeRepository, TradeTriangleEntity, TradeTriangleRepository } from '../entity';
4 |
5 | export const allEntityTypes: ObjectType[] = [TradeTriangleEntity, TradeEdgeEntity];
6 |
7 | export const allRepositoryTypes: ObjectType[] = [TradeTriangleRepository, TradeEdgeRepository];
8 |
--------------------------------------------------------------------------------
/libs/persistence/common/get-connection-options.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '@ta2-libs/config';
2 | import { ConnectionOptions } from 'typeorm';
3 | import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions';
4 |
5 | import { allEntityTypes } from './constants';
6 |
7 | export function getConnectionOptions(): ConnectionOptions {
8 | return {
9 | ...Config.connectionOptions,
10 | entities: allEntityTypes,
11 | } as MysqlConnectionOptions;
12 | }
13 |
--------------------------------------------------------------------------------
/libs/persistence/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from './transformers';
2 | export * from './constants';
3 | export * from './get-connection-options';
4 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/entity-test-bed/index.ts:
--------------------------------------------------------------------------------
1 | export * from './entity-test-bed';
2 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/entity-test-bed/test-helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './override-timestamp-columns';
2 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/entity-test-bed/test-helpers/override-timestamp-columns.spec.ts:
--------------------------------------------------------------------------------
1 | import { overrideTimestampColumns } from './override-timestamp-columns';
2 |
3 | describe('overrideTimestampColumns', () => {
4 | describe('When shallow object', () => {
5 | it('should override target key', () => {
6 | expect(
7 | overrideTimestampColumns({
8 | timestamp: 1,
9 | }),
10 | ).toEqual({
11 | timestamp: 'overridden',
12 | });
13 | });
14 | });
15 |
16 | describe('When deep object', () => {
17 | it('should override target key', () => {
18 | expect(
19 | overrideTimestampColumns({
20 | deep: {
21 | timestamp: 1,
22 | },
23 | }),
24 | ).toEqual({
25 | deep: {
26 | timestamp: 'overridden',
27 | },
28 | });
29 | });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/entity-test-bed/test-helpers/override-timestamp-columns.ts:
--------------------------------------------------------------------------------
1 | import { difference, isObject, pick } from 'lodash';
2 |
3 | /**
4 | * Override autogenerated timestamp columns with provided values.
5 | *
6 | * @param target
7 | * @param valueToOverride
8 | */
9 | export function overrideTimestampColumns(target: T, valueToOverride: any = 'overridden'): T {
10 | if (target === null || target === undefined || typeof target === 'string' || typeof target === 'number' || typeof target === 'boolean') {
11 | return target;
12 | }
13 |
14 | if (Array.isArray(target)) {
15 | target.forEach((v: any) => overrideTimestampColumns(v, valueToOverride));
16 | } else {
17 | overrideProperties(target, valueToOverride);
18 | }
19 |
20 | return target;
21 | }
22 |
23 | function overrideProperties(object: { [key: string]: any }, valueToOverride: any): void {
24 | Object.keys(object).forEach((key) => {
25 | if (isObject(object[key])) {
26 | overrideProperties(object[key], valueToOverride);
27 | } else {
28 | switch (key) {
29 | case 'createdAt':
30 | case 'updatedAt':
31 | case 'executedAt':
32 | case 'canceledAt':
33 | case 'orderedAt':
34 | case 'tickAt':
35 | case 'timestamp':
36 | case 'lastExecutedAt': {
37 | object[key] = valueToOverride;
38 |
39 | break;
40 | }
41 | default: {
42 | // noop
43 | }
44 | }
45 | }
46 | });
47 | }
48 |
49 | /**
50 | * Override autogenerated columns with provided values.
51 | *
52 | * @param target
53 | * @param attrs
54 | */
55 | export function unpickAutoGenColumns(target: T, attrs?: string[]): T {
56 | if (!isIterable(target)) {
57 | return target;
58 | }
59 |
60 | const attrsToUnpick = attrs ? attrs : ['createdAt', 'updatedAt', 'id'];
61 |
62 | const pickImpl = (ob: any): object => {
63 | return pick(ob, difference(Object.keys(ob), attrsToUnpick));
64 | };
65 |
66 | if (Array.isArray(target)) {
67 | return ((target as any[]).map(pickImpl) as unknown) as T;
68 | } else {
69 | return ([target].map(pickImpl)[0] as unknown) as T;
70 | }
71 | }
72 |
73 | function isIterable(obj: any): boolean {
74 | // checks for null and undefined
75 | if (obj == null) {
76 | return false;
77 | }
78 |
79 | return typeof obj[Symbol.iterator] === 'function';
80 | }
81 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/index.ts:
--------------------------------------------------------------------------------
1 | export * from './entity-test-bed';
2 | export * from './test-helpers';
3 | export * from './mock.data';
4 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/mock.data.ts:
--------------------------------------------------------------------------------
1 | import { TradeStatus } from '@ta2-libs/models';
2 | import { TradeTriangleEntityParam } from '@ta2-libs/persistence';
3 | import { TradeEdgeEntityParam } from '@ta2-libs/persistence/entity/trade-edge/trade-edge.repository';
4 |
5 | export const mockTradeTriangleEntityParam: TradeTriangleEntityParam = {
6 | id: 'BUSD-ETH-UFT_1626796988971',
7 | edge1Id: 'BUSD-ETH_1626796988971',
8 | edge2Id: 'ETH-UFT_1626796988971',
9 | edge3Id: 'BUSD-ETH_1626796988971',
10 | rate: '0.54189005',
11 | status: TradeStatus.Todo,
12 | };
13 |
14 | export const mockTradeEdgeEntityParam: TradeEdgeEntityParam = {
15 | id: 'BUSD-ETH_1626796988971',
16 | triangleId: 'BUSD-ETH-UFT_1626796988971',
17 | status: TradeStatus.Todo,
18 | pair: 'BTC/BUSD',
19 | fromAsset: 'BUSD',
20 | toAsset: 'BTC',
21 | side: 'buy',
22 | price: 43454.02,
23 | quantity: 0.002081,
24 | fee: 0.001,
25 | };
26 |
27 | export const mockTradeEdgeEntityParam2: TradeEdgeEntityParam = {
28 | id: 'BUSD-ETH_1626796988971',
29 | triangleId: 'BUSD-ETH-UFT_1626796988971',
30 | status: TradeStatus.Todo,
31 | pair: 'ALPACA/BUSD',
32 | fromAsset: 'BUSD',
33 | toAsset: 'BTC',
34 | side: 'buy',
35 | price: 43454.02,
36 | quantity: 0.002081,
37 | fee: 0.001,
38 | };
39 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/test-helpers/export-as-json-file.ts:
--------------------------------------------------------------------------------
1 | import { writeFile } from 'fs';
2 | import { promisify } from 'util';
3 |
4 | const writeFileAsync = promisify(writeFile);
5 |
6 | export async function exportAsJsonFile(path: string, data: any): Promise {
7 | const contents = JSON.stringify(data, null, 2);
8 |
9 | await writeFileAsync(path, contents);
10 | }
11 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/test-helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './override-timestamp-columns';
2 | export { exportAsJsonFile } from './export-as-json-file';
3 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/test-helpers/override-timestamp-columns.spec.ts:
--------------------------------------------------------------------------------
1 | import { overrideTimestampColumns } from '@dripjs/testing';
2 |
3 | describe('overrideTimestampColumns', () => {
4 | describe('When shallow object', () => {
5 | it('should override target key', () => {
6 | expect(
7 | overrideTimestampColumns({
8 | timestamp: 1,
9 | }),
10 | ).toEqual({
11 | timestamp: 'overridden',
12 | });
13 | });
14 | });
15 |
16 | describe('When deep object', () => {
17 | it('should override target key', () => {
18 | expect(
19 | overrideTimestampColumns({
20 | deep: {
21 | timestamp: 1,
22 | },
23 | }),
24 | ).toEqual({
25 | deep: {
26 | timestamp: 'overridden',
27 | },
28 | });
29 | });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/libs/persistence/common/testing/test-helpers/override-timestamp-columns.ts:
--------------------------------------------------------------------------------
1 | import { difference, isObject, pick } from 'lodash';
2 |
3 | /**
4 | * Override autogenerated timestamp columns with provided values.
5 | *
6 | * @param target
7 | * @param valueToOverride
8 | */
9 | export function overrideTimestampColumns(target: T, valueToOverride: any = 'overridden'): T {
10 | if (target === null || target === undefined || typeof target === 'string' || typeof target === 'number' || typeof target === 'boolean') {
11 | return target;
12 | }
13 |
14 | if (Array.isArray(target)) {
15 | target.forEach((v: any) => overrideTimestampColumns(v, valueToOverride));
16 | } else {
17 | overrideProperties(target, valueToOverride);
18 | }
19 |
20 | return target;
21 | }
22 |
23 | function overrideProperties(object: { [key: string]: any }, valueToOverride: any): void {
24 | Object.keys(object).forEach((key) => {
25 | if (isObject(object[key])) {
26 | overrideProperties(object[key], valueToOverride);
27 | } else {
28 | // all column names that exists in @dripjs/core-models.
29 | switch (key) {
30 | case 'createdAt':
31 | case 'updatedAt':
32 | case 'executedAt':
33 | case 'canceledAt':
34 | case 'orderedAt':
35 | case 'tickAt':
36 | case 'timestamp':
37 | case 'lastExecutedAt': {
38 | object[key] = valueToOverride;
39 |
40 | break;
41 | }
42 | default: {
43 | // noop
44 | }
45 | }
46 | }
47 | });
48 | }
49 |
50 | /**
51 | * Override autogenerated columns with provided values.
52 | *
53 | * @param target
54 | * @param attrs
55 | */
56 | export function unpickAutoGenColumns(target: T, attrs?: string[]): T {
57 | if (!isIterable(target)) {
58 | return target;
59 | }
60 |
61 | const attrsToUnpick = attrs ? attrs : ['createdAt', 'updatedAt', 'id'];
62 |
63 | const pickImpl = (ob: any): object => {
64 | return pick(ob, difference(Object.keys(ob), attrsToUnpick));
65 | };
66 |
67 | if (Array.isArray(target)) {
68 | return ((target as any[]).map(pickImpl) as unknown) as T;
69 | } else {
70 | return ([target].map(pickImpl)[0] as unknown) as T;
71 | }
72 | }
73 |
74 | function isIterable(obj: any): boolean {
75 | // checks for null and undefined
76 | if (obj == null) {
77 | return false;
78 | }
79 |
80 | return typeof obj[Symbol.iterator] === 'function';
81 | }
82 |
--------------------------------------------------------------------------------
/libs/persistence/common/transformers.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | booleanTinyintTransformer,
3 | getFloorByDigitsTransformer,
4 | nullableBooleanTinyintTransformer,
5 | nullableDateTransformer,
6 | } from './transformers';
7 |
8 | describe('transformers', () => {
9 | it('getFloorByDigitsTransformer', async () => {
10 | const transformer = getFloorByDigitsTransformer(4);
11 | expect(transformer.from(null)).toEqual(null);
12 | expect(transformer.to(null)).toEqual(null);
13 | expect(transformer.to('43454.0001800')).toEqual('43454.0001');
14 | expect(transformer.to('43454.0010800')).toEqual('43454.001');
15 | expect(transformer.to('0.00015')).toEqual('0.0001');
16 | expect(transformer.to('1')).toEqual('1');
17 | });
18 |
19 | it('nullableDateTransformer', async () => {
20 | const date = new Date();
21 | expect(nullableDateTransformer.from(null)).toEqual(null);
22 | expect(nullableDateTransformer.from(date)).toEqual(date.getTime());
23 | expect(nullableDateTransformer.from(date.getTime())).toEqual(date.getTime());
24 | expect(nullableDateTransformer.to(null)).toEqual(null);
25 | expect(nullableDateTransformer.to(date.getTime())).toEqual(date);
26 | });
27 |
28 | it('booleanTinyintTransformer', async () => {
29 | expect(booleanTinyintTransformer.from(0)).toBeFalsy();
30 | expect(booleanTinyintTransformer.from(1)).toBeTruthy();
31 | expect(booleanTinyintTransformer.to(null)).toEqual(null);
32 | try {
33 | expect(booleanTinyintTransformer.to(NaN)).toEqual(new Error(`Invalid boolean value NaN`));
34 | } catch (e) {}
35 | expect(booleanTinyintTransformer.to(false)).toEqual(0);
36 | expect(booleanTinyintTransformer.to(true)).toEqual(1);
37 | });
38 |
39 | it('nullableBooleanTinyintTransformer', async () => {
40 | expect(nullableBooleanTinyintTransformer.from(0)).toBeFalsy();
41 | expect(nullableBooleanTinyintTransformer.from(1)).toBeTruthy();
42 | expect(nullableBooleanTinyintTransformer.to(null)).toEqual(null);
43 | expect(nullableBooleanTinyintTransformer.to(false)).toEqual(0);
44 | expect(nullableBooleanTinyintTransformer.to(true)).toEqual(1);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/libs/persistence/common/transformers.ts:
--------------------------------------------------------------------------------
1 | type StringTransformerFunction = (value?: string | number | null | undefined) => string | number | null | undefined;
2 |
3 | interface StringTransformer {
4 | from: StringTransformerFunction;
5 | to: StringTransformerFunction;
6 | }
7 |
8 | export function getFloorByDigitsTransformer(digit: number): StringTransformer {
9 | return {
10 | from: (value?: string | number | null | undefined) => value,
11 | to: (value?: string | number | null | undefined) => {
12 | if (value === undefined || value === null) {
13 | return value;
14 | }
15 |
16 | const [integer, fraction] = `${value}`.split('.');
17 | if (fraction === undefined) {
18 | return value;
19 | }
20 |
21 | const fractionReduced = fraction.substr(0, digit);
22 |
23 | return `${integer}.${fractionReduced}`;
24 | },
25 | };
26 | }
27 |
28 | export const nullableDateTransformer = {
29 | from: (value?: Date | null | undefined | number) => {
30 | if (value === undefined || value === null) {
31 | return value;
32 | }
33 |
34 | if (value instanceof Date) {
35 | return value.getTime();
36 | }
37 |
38 | return value;
39 | },
40 | to: (value?: number | null | undefined) => {
41 | if (value === undefined || value === null) {
42 | return value;
43 | }
44 |
45 | return new Date(value);
46 | },
47 | };
48 |
49 | /**
50 | * Use this transformer for the following case.
51 | * - we want to use a value as `boolean` in application, but store it as `tinyint` into DB.
52 | */
53 | export const booleanTinyintTransformer = {
54 | from: (val: 0 | 1): boolean => !!val,
55 | to: (val: boolean) => {
56 | if (val === undefined || val === null) {
57 | return val;
58 | }
59 | const n = Number(val);
60 | if (Number.isNaN(n)) {
61 | throw new Error(`Invalid boolean value ${val}`);
62 | }
63 |
64 | return n as 0 | 1;
65 | },
66 | };
67 |
68 | /**
69 | * Use this when tinyint has a default value i.e. '0' or '1'.
70 | * @see MasterPairEntity
71 | */
72 | export const nullableBooleanTinyintTransformer = {
73 | from: (val: 0 | 1): boolean => !!val,
74 | to: (val: boolean | null | undefined) => {
75 | if (val === undefined || val === null) {
76 | return val;
77 | }
78 |
79 | return Number(val) as 0 | 1;
80 | },
81 | };
82 |
--------------------------------------------------------------------------------
/libs/persistence/entity/index.ts:
--------------------------------------------------------------------------------
1 | export * from './trade-triangle';
2 | export * from './trade-edge';
3 |
--------------------------------------------------------------------------------
/libs/persistence/entity/trade-edge/index.ts:
--------------------------------------------------------------------------------
1 | export * from './trade-edge.entity';
2 | export * from './trade-edge.repository';
3 |
--------------------------------------------------------------------------------
/libs/persistence/entity/trade-edge/trade-edge.entity.ts:
--------------------------------------------------------------------------------
1 | import { Timestamp, TradeStatus } from '@ta2-libs/models';
2 | import { Column, Entity, PrimaryColumn } from 'typeorm';
3 |
4 | import { getFloorByDigitsTransformer, nullableDateTransformer } from '../../common';
5 |
6 | @Entity({
7 | name: 'trade_edge',
8 | })
9 | export class TradeEdgeEntity {
10 | @PrimaryColumn({
11 | type: 'varchar',
12 | name: 'id',
13 | length: 30,
14 | comment: 'edge id (BUSD-ETH_1626796988971)',
15 | })
16 | readonly id!: string;
17 |
18 | @Column({
19 | type: 'varchar',
20 | name: 'triangle_id',
21 | length: 30,
22 | comment: 'triangle id (BUSD-ETH-UFT_1626796988971)',
23 | })
24 | readonly triangleId!: string;
25 |
26 | @Column({
27 | type: 'varchar',
28 | name: 'pair',
29 | length: 20,
30 | comment: 'pair',
31 | })
32 | readonly pair!: string;
33 |
34 | @Column({
35 | type: 'varchar',
36 | name: 'from_asset',
37 | length: 10,
38 | comment: 'from asset',
39 | })
40 | readonly fromAsset!: string;
41 |
42 | @Column({
43 | type: 'varchar',
44 | name: 'to_asset',
45 | length: 10,
46 | comment: 'to asset',
47 | })
48 | readonly toAsset!: string;
49 |
50 | @Column({
51 | type: 'varchar',
52 | name: 'side',
53 | length: 10,
54 | comment: 'side',
55 | })
56 | readonly side!: 'sell' | 'buy';
57 |
58 | @Column({
59 | type: 'decimal',
60 | name: 'price',
61 | precision: 21,
62 | scale: 9,
63 | unsigned: true,
64 | comment: 'price',
65 | default: /* istanbul ignore next */ () => '0.0',
66 | transformer: getFloorByDigitsTransformer(9),
67 | })
68 | readonly price!: number;
69 |
70 | @Column({
71 | type: 'decimal',
72 | name: 'quantity',
73 | precision: 21,
74 | scale: 9,
75 | unsigned: true,
76 | comment: 'quantity',
77 | default: /* istanbul ignore next */ () => '0.0',
78 | transformer: getFloorByDigitsTransformer(9),
79 | })
80 | readonly quantity!: number;
81 |
82 | @Column({
83 | type: 'decimal',
84 | name: 'fee',
85 | precision: 21,
86 | scale: 9,
87 | unsigned: true,
88 | comment: 'fee',
89 | default: /* istanbul ignore next */ () => '0.0',
90 | transformer: getFloorByDigitsTransformer(9),
91 | })
92 | readonly fee!: number;
93 |
94 | @Column({
95 | type: 'varchar',
96 | name: 'status',
97 | length: 10,
98 | comment: 'status',
99 | })
100 | readonly status!: TradeStatus;
101 |
102 | @Column({
103 | type: 'datetime',
104 | name: 'created_at',
105 | precision: 3,
106 | default: /* istanbul ignore next */ () => 'NOW(3)',
107 | transformer: nullableDateTransformer,
108 | })
109 | readonly createdAt!: Timestamp;
110 |
111 | @Column({
112 | type: 'datetime',
113 | name: 'updated_at',
114 | precision: 3,
115 | default: /* istanbul ignore next */ () => 'NOW(3)',
116 | onUpdate: 'NOW(3)',
117 | transformer: nullableDateTransformer,
118 | })
119 | readonly updatedAt!: Timestamp;
120 | }
121 |
--------------------------------------------------------------------------------
/libs/persistence/entity/trade-edge/trade-edge.repository.spec.ts:
--------------------------------------------------------------------------------
1 | import { TradeEdgeEntity } from '@ta2-libs/persistence';
2 | import { mockTradeEdgeEntityParam, mockTradeEdgeEntityParam2 } from '@ta2-libs/persistence/common/testing';
3 |
4 | import { EntityTestBed } from '../../common/testing/entity-test-bed';
5 | import { TradeEdgeEntityParam, TradeEdgeRepository } from './trade-edge.repository';
6 |
7 | describe('trade-edge.repository', () => {
8 | let repository: TradeEdgeRepository;
9 | const defaultData = mockTradeEdgeEntityParam;
10 | const defaultData2 = mockTradeEdgeEntityParam2;
11 |
12 | beforeAll(async () => {
13 | await EntityTestBed.setup();
14 | repository = EntityTestBed.getRepository(TradeEdgeRepository);
15 | });
16 |
17 | afterAll(async () => {
18 | await EntityTestBed.cleanup();
19 | });
20 |
21 | beforeEach(async () => {
22 | await EntityTestBed.reset();
23 | await repository.insertTradeEdge(defaultData);
24 | });
25 |
26 | describe('insertTradeEdge', () => {
27 | it('should insert new trade edge', async () => {
28 | const newData = {
29 | ...defaultData,
30 | id: 'insert-data',
31 | };
32 | await repository.insertTradeEdge(newData);
33 | const insertedData = await repository.find({
34 | id: newData.id,
35 | });
36 | expect(insertedData.map(getDataFromEntity)).toEqual([newData]);
37 | });
38 |
39 | it('should insert new trade edge 2', async () => {
40 | const newData = {
41 | ...defaultData2,
42 | id: 'insert-data',
43 | };
44 | await repository.insertTradeEdge(newData);
45 | const insertedData = await repository.find({
46 | id: newData.id,
47 | });
48 | expect(insertedData.map(getDataFromEntity)).toEqual([newData]);
49 | });
50 | });
51 |
52 | describe('insertTradeEdges', () => {
53 | it('should insert new trade edges', async () => {
54 | const newData = {
55 | ...defaultData,
56 | id: 'insert-data',
57 | };
58 | const res = await repository.insertTradeEdges([
59 | {
60 | pair: 'BTC/BUSD',
61 | fromAsset: 'BUSD',
62 | toAsset: 'BTC',
63 | side: 'buy',
64 | price: 45069.77,
65 | quantity: 0.002021,
66 | fee: 0.001,
67 | id: 'BTCBUSD_1629314958989',
68 | status: 'todo',
69 | triangleId: 'BUSD-BTC-ETH_1629314958990',
70 | },
71 | {
72 | pair: 'ETH/BTC',
73 | fromAsset: 'BTC',
74 | toAsset: 'ETH',
75 | side: 'buy',
76 | price: 0.067361,
77 | quantity: 0.029,
78 | fee: 0.001,
79 | id: 'ETHBTC_1629314958990',
80 | status: 'todo',
81 | triangleId: 'BUSD-BTC-ETH_1629314958990',
82 | },
83 | {
84 | pair: 'ETH/BUSD',
85 | fromAsset: 'ETH',
86 | toAsset: 'BUSD',
87 | side: 'sell',
88 | price: 3036.41,
89 | quantity: 0.028,
90 | fee: 0.001,
91 | id: 'ETHBUSD_1629314958990',
92 | status: 'todo',
93 | triangleId: 'BUSD-BTC-ETH_1629314958990',
94 | },
95 | ]);
96 | console.log(res);
97 | const insertedData = await repository.find({
98 | id: newData.id,
99 | });
100 | expect(insertedData.map(getDataFromEntity)).toEqual([newData]);
101 | });
102 | });
103 |
104 | describe('updateTradeEdge', () => {
105 | const updData = {
106 | ...defaultData,
107 | fee: 0.8,
108 | };
109 | it('should update TradeEdge', async () => {
110 | const res = await repository.updateTradeEdge(updData);
111 | expect(res.affected).toEqual(1);
112 | const updatedData = await repository.find();
113 | expect(updatedData.map(getDataFromEntity)).toEqual([updData]);
114 | });
115 | });
116 |
117 | describe('getTradeEdge', () => {
118 | it('should get TradeEdge', async () => {
119 | const res = await repository.getTradeEdge(defaultData.id);
120 | expect(getDataFromEntity(res)).toEqual(defaultData);
121 | });
122 | });
123 | });
124 |
125 | function getDataFromEntity(entity: TradeEdgeEntity): TradeEdgeEntityParam {
126 | return {
127 | id: entity.id,
128 | triangleId: entity.triangleId,
129 | pair: entity.pair,
130 | fromAsset: entity.fromAsset,
131 | toAsset: entity.toAsset,
132 | status: entity.status,
133 | side: entity.side,
134 | price: +entity.price,
135 | quantity: +entity.quantity,
136 | fee: +entity.fee,
137 | };
138 | }
139 |
--------------------------------------------------------------------------------
/libs/persistence/entity/trade-edge/trade-edge.repository.ts:
--------------------------------------------------------------------------------
1 | import { TradeStatus } from '@ta2-libs/models';
2 | import { EntityRepository, InsertResult, Repository, UpdateResult } from 'typeorm';
3 |
4 | import { TradeEdgeEntity } from './trade-edge.entity';
5 |
6 | export type TradeEdgeEntityParam = Omit;
7 |
8 | @EntityRepository(TradeEdgeEntity)
9 | export class TradeEdgeRepository extends Repository {
10 | insertTradeEdge(param: TradeEdgeEntityParam): Promise {
11 | return this.insert({ ...param });
12 | }
13 |
14 | updateTradeEdge(param: Partial): Promise {
15 | return this.update(param.id, param);
16 | }
17 |
18 | insertTradeEdges(params: TradeEdgeEntityParam[]): Promise {
19 | return this.save([...params] as TradeEdgeEntity[], { reload: false });
20 | }
21 |
22 | getTradeEdge(id: string): Promise {
23 | return this.findOne({ where: { id } });
24 | }
25 |
26 | getActiveTradeEdges(): Promise {
27 | return this.find({ where: { status: TradeStatus.Open } });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/libs/persistence/entity/trade-triangle/index.ts:
--------------------------------------------------------------------------------
1 | export * from './trade-triangle.entity';
2 | export * from './trade-triangle.repository';
3 |
--------------------------------------------------------------------------------
/libs/persistence/entity/trade-triangle/trade-triangle.entity.ts:
--------------------------------------------------------------------------------
1 | import { Timestamp, TradeStatus } from '@ta2-libs/models';
2 | import { Column, Entity, PrimaryColumn } from 'typeorm';
3 |
4 | import { nullableDateTransformer } from '../../common';
5 |
6 | @Entity({
7 | name: 'trade_triangle',
8 | })
9 | export class TradeTriangleEntity {
10 | @PrimaryColumn({
11 | type: 'varchar',
12 | name: 'id',
13 | length: 30,
14 | comment: 'triangle id (BUSD-ETH-UFT_1626796988971)',
15 | })
16 | readonly id!: string;
17 |
18 | @Column({
19 | type: 'varchar',
20 | name: 'edge1_id',
21 | length: 30,
22 | comment: 'edge A id (BUSD-ETH_1626796988971)',
23 | })
24 | readonly edge1Id!: string;
25 |
26 | @Column({
27 | type: 'varchar',
28 | name: 'edge2_id',
29 | length: 30,
30 | comment: 'edge B id (ETH-UFT_1626796988971)',
31 | })
32 | readonly edge2Id!: string;
33 |
34 | @Column({
35 | type: 'varchar',
36 | name: 'edge3_id',
37 | length: 30,
38 | comment: 'edge C id (BUSD-UFT_1626796988971)',
39 | })
40 | readonly edge3Id!: string;
41 |
42 | @Column({
43 | type: 'varchar',
44 | name: 'rate',
45 | length: 20,
46 | comment: 'rate',
47 | })
48 | readonly rate!: string;
49 |
50 | @Column({
51 | type: 'varchar',
52 | name: 'status',
53 | length: 10,
54 | comment: 'status',
55 | })
56 | readonly status!: TradeStatus;
57 |
58 | @Column({
59 | type: 'datetime',
60 | name: 'created_at',
61 | precision: 3,
62 | default: /* istanbul ignore next */ () => 'NOW(3)',
63 | transformer: nullableDateTransformer,
64 | })
65 | readonly createdAt!: Timestamp;
66 |
67 | @Column({
68 | type: 'datetime',
69 | name: 'updated_at',
70 | precision: 3,
71 | default: /* istanbul ignore next */ () => 'NOW(3)',
72 | onUpdate: 'NOW(3)',
73 | transformer: nullableDateTransformer,
74 | })
75 | readonly updatedAt!: Timestamp;
76 | }
77 |
--------------------------------------------------------------------------------
/libs/persistence/entity/trade-triangle/trade-triangle.repository.spec.ts:
--------------------------------------------------------------------------------
1 | import { TradeTriangleEntity } from '@ta2-libs/persistence';
2 | import { mockTradeTriangleEntityParam } from '@ta2-libs/persistence/common/testing';
3 |
4 | import { EntityTestBed } from '../../common/testing/entity-test-bed';
5 | import { TradeTriangleEntityParam, TradeTriangleRepository } from './trade-triangle.repository';
6 |
7 | describe('trade-triangle.repository', () => {
8 | let repository: TradeTriangleRepository;
9 | const defaultData = mockTradeTriangleEntityParam;
10 |
11 | beforeAll(async () => {
12 | await EntityTestBed.setup();
13 | repository = EntityTestBed.getRepository(TradeTriangleRepository);
14 | });
15 |
16 | afterAll(async () => {
17 | await EntityTestBed.cleanup();
18 | });
19 |
20 | beforeEach(async () => {
21 | await EntityTestBed.reset();
22 | await repository.insertTradeTriangle(defaultData);
23 | });
24 |
25 | describe('insertTradeTriangle', () => {
26 | it('should insert new trade triangle', async () => {
27 | const newData = {
28 | ...defaultData,
29 | id: 'insert-data',
30 | rate: '0.8',
31 | };
32 | await repository.insertTradeTriangle(newData);
33 | const insertedData = await repository.find({
34 | id: newData.id,
35 | });
36 | expect(insertedData.map(getDataFromEntity)).toEqual([newData]);
37 | });
38 | });
39 |
40 | describe('updateTradeTriangle', () => {
41 | const updData = {
42 | ...defaultData,
43 | rate: '0.8',
44 | };
45 | it('should update TradeTriangle', async () => {
46 | const res = await repository.updateTradeTriangle(updData);
47 | expect(res.affected).toEqual(1);
48 | const updatedData = await repository.find();
49 | expect(updatedData.map(getDataFromEntity)).toEqual([updData]);
50 | });
51 | });
52 |
53 | describe('getTradeTriangles', () => {
54 | it('should get tradeTriangles', async () => {
55 | const res = await repository.getTradeTriangles(defaultData.id);
56 | expect([getDataFromEntity(res[0])]).toEqual([defaultData]);
57 | });
58 | });
59 |
60 | describe('getTradeTriangleByEdgeId', () => {
61 | it('should get tradeTriangleByEdgeId', async () => {
62 | const res = await repository.getTradeTriangleByEdgeId(defaultData.edge1Id);
63 | expect(getDataFromEntity(res)).toEqual(defaultData);
64 | const res2 = await repository.getTradeTriangleByEdgeId(defaultData.edge2Id);
65 | expect(getDataFromEntity(res2)).toEqual(defaultData);
66 | const res3 = await repository.getTradeTriangleByEdgeId(defaultData.edge3Id);
67 | expect(getDataFromEntity(res3)).toEqual(defaultData);
68 | });
69 | });
70 | });
71 |
72 | function getDataFromEntity(entity: TradeTriangleEntity): TradeTriangleEntityParam {
73 | return {
74 | id: entity.id,
75 | edge1Id: entity.edge1Id,
76 | edge2Id: entity.edge2Id,
77 | edge3Id: entity.edge3Id,
78 | rate: entity.rate,
79 | status: entity.status,
80 | };
81 | }
82 |
--------------------------------------------------------------------------------
/libs/persistence/entity/trade-triangle/trade-triangle.repository.ts:
--------------------------------------------------------------------------------
1 | import { TradeStatus } from '@ta2-libs/models';
2 | import { EntityRepository, InsertResult, Repository, UpdateResult } from 'typeorm';
3 |
4 | import { TradeTriangleEntity } from './trade-triangle.entity';
5 |
6 | export type TradeTriangleEntityParam = Omit;
7 |
8 | @EntityRepository(TradeTriangleEntity)
9 | export class TradeTriangleRepository extends Repository {
10 | insertTradeTriangle(param: TradeTriangleEntityParam): Promise {
11 | return this.insert({ ...param });
12 | }
13 |
14 | updateTradeTriangle(param: Partial): Promise {
15 | return this.update(param.id, param);
16 | }
17 |
18 | insertTradeTriangles(params: TradeTriangleEntityParam[]): Promise {
19 | return this.save([...params] as TradeTriangleEntity[], { reload: false });
20 | }
21 |
22 | getTradeTriangles(id: string): Promise {
23 | return this.find({ where: { id } });
24 | }
25 |
26 | getActiveTradeTriangles(): Promise {
27 | return this.find({ where: { status: TradeStatus.Open } });
28 | }
29 |
30 | getTradeTriangleByEdgeId(edgeId: string): Promise {
31 | return this.createQueryBuilder('triangle')
32 | .where('triangle.edge1Id = :id or triangle.edge2Id = :id or triangle.edge3Id = :id', { id: edgeId })
33 | .getOne();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/libs/persistence/index.ts:
--------------------------------------------------------------------------------
1 | export * from './common';
2 | export * from './entity';
3 | export * from './persistence.module';
4 |
--------------------------------------------------------------------------------
/libs/persistence/persistence.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Global, Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 |
4 | import { getConnectionOptions } from './common';
5 |
6 | @Global()
7 | @Module({})
8 | export class PersistenceModule {
9 | static forRoot(): DynamicModule {
10 | return {
11 | module: PersistenceModule,
12 | imports: [TypeOrmModule.forRoot(getConnectionOptions())],
13 | };
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/libs/testing/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mock';
2 | export * from './utils';
3 |
--------------------------------------------------------------------------------
/libs/testing/mock/data/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mock-pairs';
2 | export * from './mock-pair-fees';
3 | export * from './mock-order';
4 | export * from './mock-user-data';
5 |
--------------------------------------------------------------------------------
/libs/testing/mock/data/mock-order.ts:
--------------------------------------------------------------------------------
1 | import { Order } from 'ccxt';
2 |
3 | export const mockOrder: Order = {
4 | info: {
5 | symbol: 'ETHBTC',
6 | orderId: '2162102936',
7 | orderListId: '-1',
8 | clientOrderId: 'ETHBTC_1629472740257',
9 | transactTime: '1629472741283',
10 | price: '0.06763600',
11 | origQty: '0.02800000',
12 | executedQty: '0.02800000',
13 | cummulativeQuoteQty: '0.00189380',
14 | status: 'FILLED',
15 | timeInForce: 'GTC',
16 | type: 'LIMIT',
17 | side: 'BUY',
18 | fills: [
19 | {
20 | price: '0.06763600',
21 | qty: '0.02800000',
22 | commission: '0.00015425',
23 | commissionAsset: 'BNB',
24 | tradeId: '290543691',
25 | },
26 | ],
27 | },
28 | id: '2162102936',
29 | clientOrderId: 'ETHBTC_1629472740257',
30 | timestamp: 1629472741283,
31 | datetime: '2021-08-20T15:19:01.283Z',
32 | symbol: 'ETH/BTC',
33 | type: 'limit',
34 | timeInForce: 'GTC',
35 | postOnly: false,
36 | side: 'buy',
37 | price: 0.067636,
38 | amount: 0.028,
39 | cost: 0.0018938,
40 | average: 0.06763571428571429,
41 | filled: 0.028,
42 | remaining: 0,
43 | status: 'closed',
44 | fee: {
45 | cost: 0.00015425,
46 | currency: 'BNB',
47 | },
48 | trades: [
49 | {
50 | info: {
51 | price: '0.06763600',
52 | qty: '0.02800000',
53 | commission: '0.00015425',
54 | commissionAsset: 'BNB',
55 | tradeId: '290543691',
56 | },
57 | symbol: 'ETH/BTC',
58 | price: 0.067636,
59 | amount: 0.028,
60 | cost: 0.001893808,
61 | fee: {
62 | cost: 0.00015425,
63 | currency: 'BNB',
64 | },
65 | },
66 | ],
67 | fees: [
68 | {
69 | cost: 0.00015425,
70 | currency: 'BNB',
71 | },
72 | ],
73 | } as any;
74 |
--------------------------------------------------------------------------------
/libs/testing/mock/data/mock-user-data.ts:
--------------------------------------------------------------------------------
1 | import { UserData } from '@ta2-libs/broker-api';
2 |
3 | export const mockExecutionReport: UserData = {
4 | eventType: 'executionReport',
5 | eventTime: 1629472741284,
6 | symbol: 'ETHBTC',
7 | newClientOrderId: 'ETHBTC_1629472740257',
8 | side: 'BUY',
9 | orderType: 'LIMIT',
10 | cancelType: 'GTC',
11 | quantity: '0.02800000',
12 | price: '0.06763600',
13 | stopPrice: '0.00000000',
14 | icebergQuantity: '0.00000000',
15 | g: -1,
16 | originalClientOrderId: '',
17 | executionType: 'TRADE',
18 | orderStatus: 'FILLED',
19 | rejectReason: 'NONE',
20 | orderId: 2162102936,
21 | lastTradeQuantity: '0.02800000',
22 | accumulatedQuantity: '0.02800000',
23 | lastTradePrice: '0.06763600',
24 | commission: '0.00015425',
25 | commissionAsset: 'BNB',
26 | tradeTime: 1629472741283,
27 | tradeId: 290543691,
28 | I: 4586765566,
29 | w: false,
30 | maker: false,
31 | M: true,
32 | O: 1629472741283,
33 | Z: '0.00189380',
34 | Y: '0.00189380',
35 | Q: '0.00000000',
36 | } as any;
37 |
--------------------------------------------------------------------------------
/libs/testing/mock/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mock.module';
2 | export * from './mock.service';
3 | export * from './mock.data';
4 |
--------------------------------------------------------------------------------
/libs/testing/mock/mock.data.ts:
--------------------------------------------------------------------------------
1 | import { mockExecutionReport, mockOrder, mockPairFees, mockPairs } from './data';
2 |
3 | export const mockData = {
4 | pairInfo: mockPairs,
5 | pairFees: mockPairFees,
6 | orderResult: mockOrder,
7 | executionReport: mockExecutionReport,
8 | };
9 |
--------------------------------------------------------------------------------
/libs/testing/mock/mock.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 |
3 | import { provideMockServices } from './mock.service';
4 |
5 | /* mock services */
6 |
7 | @Global()
8 | @Module({
9 | exports: [...provideMockServices()],
10 | providers: [...provideMockServices()],
11 | })
12 | export class MockModule {}
13 |
--------------------------------------------------------------------------------
/libs/testing/mock/mock.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Logger } from '@ta2-libs/logger';
3 |
4 | @Injectable()
5 | export class MockLogger implements Partial {
6 | constructor() {}
7 | debug(...data: any[]) {
8 | // eslint-disable-next-line no-console
9 | console.debug(...data);
10 | }
11 | log(...data: any[]) {
12 | // eslint-disable-next-line no-console
13 | console.log(...data);
14 | }
15 | info(...data: any[]) {
16 | // eslint-disable-next-line no-console
17 | console.info(...data);
18 | }
19 | warn(...data: any[]) {
20 | // eslint-disable-next-line no-console
21 | console.warn(...data);
22 | }
23 | error(...data: any[]) {
24 | // eslint-disable-next-line no-console
25 | console.error(...data);
26 | }
27 | }
28 |
29 | // for storybook
30 | export function provideMockServices() {
31 | return [{ provide: Logger, useClass: MockLogger }];
32 | }
33 |
--------------------------------------------------------------------------------
/libs/testing/utils/build-defer-init-service.ts:
--------------------------------------------------------------------------------
1 | import { OnModuleInit } from '@nestjs/common';
2 |
3 | export async function buildDeferInitService(service: T): Promise {
4 | await service.onModuleInit();
5 | return service;
6 | }
7 |
--------------------------------------------------------------------------------
/libs/testing/utils/get-dummy-execution-context.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionContext } from '@nestjs/common';
2 | // tslint:disable-next-line:no-submodule-imports
3 | import { RpcArgumentsHost } from '@nestjs/common/interfaces';
4 |
5 | export function getDummyExecutionContext(args?: any, res?: any): ExecutionContext {
6 | return {
7 | getType(): any {},
8 | getArgs(): any {},
9 | getArgByIndex(): any {},
10 | switchToHttp() {
11 | return {
12 | getRequest(): any {
13 | return args ? { body: args } : null;
14 | },
15 | getResponse(): T {
16 | return res as T;
17 | },
18 | getNext(): T {
19 | return res as T;
20 | },
21 | };
22 | },
23 | switchToRpc(): RpcArgumentsHost {
24 | return {
25 | getData(): T {
26 | return args as T;
27 | },
28 | getContext(): T {
29 | return args as T;
30 | },
31 | };
32 | },
33 | switchToWs(): any {},
34 | getClass(): any {},
35 | getHandler(): any {},
36 | };
37 | }
38 |
39 | export const createArgumentsHost = (): any => {
40 | return {
41 | switchToHttp: () => {
42 | return {
43 | getResponse: () => {
44 | return {
45 | status: (...args: any) => {
46 | return {
47 | json: (...json: any) => {},
48 | };
49 | },
50 | };
51 | },
52 | };
53 | },
54 | };
55 | };
56 |
--------------------------------------------------------------------------------
/libs/testing/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './build-defer-init-service';
2 | export * from './get-dummy-execution-context';
3 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "apps/triangular-arbitrage2/src",
4 | "monorepo": true,
5 | "root": "apps/triangular-arbitrage2",
6 | "compilerOptions": {
7 | "webpack": true,
8 | "tsConfigPath": "apps/triangular-arbitrage2/tsconfig.app.json"
9 | },
10 | "projects": {
11 | "cli-app": {
12 | "type": "application",
13 | "root": "apps/cli-app",
14 | "entryFile": "main",
15 | "sourceRoot": "apps/cli-app/src",
16 | "compilerOptions": {
17 | "tsConfigPath": "apps/cli-app/tsconfig.app.json"
18 | }
19 | },
20 | "pro-cli-app": {
21 | "type": "application",
22 | "root": "apps/pro-cli-app",
23 | "entryFile": "main",
24 | "sourceRoot": "apps/pro-cli-app/src",
25 | "compilerOptions": {
26 | "tsConfigPath": "apps/pro-cli-app/tsconfig.app.json"
27 | }
28 | },
29 | "ta2-libs": {
30 | "type": "library",
31 | "root": "libs",
32 | "entryFile": "index",
33 | "sourceRoot": "libs"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ormconfig.js:
--------------------------------------------------------------------------------
1 | const config = require('config');
2 |
3 | // used by typeorm cli
4 | module.exports = {
5 | ...config.mysql,
6 | type: 'mysql',
7 | name: 'default',
8 | dropSchema: false,
9 | synchronize: false,
10 | migrationsRun: false,
11 | supportBigNumbers: true,
12 | bigNumberStrings: true,
13 | entities: ['dist/libs/persistence/entity/**/*.entity.js'],
14 | };
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "triangular-arbitrage2",
3 | "version": "0.1.0",
4 | "description": "a server side application for perform triangular arbitrage.",
5 | "author": "zlq4863947@gmail.com",
6 | "license": "GPL3",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/zlq4863947/triangular-arbitrage2.git"
10 | },
11 | "scripts": {
12 | "deploy:all": "yarn deploy && gulp zip && gulp gzip",
13 | "deploy:linux": "yarn deploy && gulp gzip",
14 | "deploy:windows": "yarn deploy && gulp zip",
15 | "deploy": "yarn prebuild && yarn build && yarn bundle",
16 | "bundle": "gulp bundle",
17 | "build": "gulp build",
18 | "prebuild": "rimraf dist && gulp minify",
19 | "db:sync": "yarn build && yarn _schema:sync",
20 | "docker:up": "docker-compose -f ./docker-compose.yml up -d",
21 | "docker:down": "docker-compose -f ./docker-compose.yml down",
22 | "format": "yarn format:ts:import && yarn format:ts",
23 | "format:ts": "prettier --config .prettierrc --write \"{apps,libs,tools}/**/*.ts\"",
24 | "format:ts:import": "import-sort --write \"{apps,libs}/**/*.ts\"",
25 | "webpack:build": "webpack --config webpack/deploy.config.js --env",
26 | "start:cli": "cross-env NODE_ENV=prod ts-node -r tsconfig-paths/register apps/cli-app/src/main.ts",
27 | "start:pro-cli": "cross-env NODE_ENV=prod ts-node -r tsconfig-paths/register apps/pro-cli-app/src/main.ts",
28 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
29 | "jest": "jest --config=jest.config.js --detectOpenHandles",
30 | "test": "cross-env NODE_ENV=test yarn jest -- --runInBand",
31 | "test:cov": "yarn jest -- --coverage",
32 | "test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
33 | "test:e2e": "jest --config apps/cli-app/test/jest-e2e.json",
34 | "_schema:sync": "yarn ts-node $(npm bin)/typeorm schema:sync"
35 | },
36 | "dependencies": {
37 | "@nestjs/common": "^7.6.15",
38 | "@nestjs/core": "^7.6.15",
39 | "@nestjs/platform-express": "^7.6.15",
40 | "@nestjs/typeorm": "^8.0.2",
41 | "ascii-table": "^0.0.9",
42 | "axios": "^0.21.2",
43 | "bignumber.js": "^9.0.1",
44 | "binance": "^1.3.7",
45 | "ccxt": "^1.50.24",
46 | "chalk": "^4.1.1",
47 | "circular-json": "^0.5.9",
48 | "config": "^3.3.6",
49 | "cross-env": "^7.0.3",
50 | "glob-parent": "^5.1.2",
51 | "lodash": "^4.17.21",
52 | "log4js": "^6.3.0",
53 | "moment": "^2.29.1",
54 | "mysql2": "^2.3.0",
55 | "nodemailer": "^6.6.3",
56 | "path-parse": "^1.0.7",
57 | "reflect-metadata": "^0.1.13",
58 | "rimraf": "^3.0.2",
59 | "rxjs": "^7",
60 | "toml": "^3.0.0",
61 | "typeorm": "^0.2.36",
62 | "ws": "^7.4.6",
63 | "yargs-parser": "^13.1.2"
64 | },
65 | "devDependencies": {
66 | "@nestjs/cli": "^7.6.0",
67 | "@nestjs/schematics": "^7.3.0",
68 | "@nestjs/testing": "^7.6.15",
69 | "@types/express": "^4.17.11",
70 | "@types/gulp": "^4.0.8",
71 | "@types/jest": "^26.0.22",
72 | "@types/lodash": "^4.14.172",
73 | "@types/node": "^14.14.36",
74 | "@types/nodemailer": "^6.4.4",
75 | "@types/supertest": "^2.0.10",
76 | "@types/ws": "^7.4.4",
77 | "@typescript-eslint/eslint-plugin": "^4.19.0",
78 | "@typescript-eslint/parser": "^4.19.0",
79 | "eslint": "^7.22.0",
80 | "eslint-config-prettier": "^8.1.0",
81 | "eslint-plugin-prettier": "^3.3.1",
82 | "glob": "^7.1.7",
83 | "gulp": "^4.0.2",
84 | "gulp-gzip": "^1.4.2",
85 | "gulp-tar": "^3.1.0",
86 | "gulp-terser": "^2.0.1",
87 | "gulp-ts-alias": "^1.3.0",
88 | "gulp-typescript": "^6.0.0-alpha.1",
89 | "gulp-zip": "^5.1.0",
90 | "import-sort": "5.0.0",
91 | "import-sort-cli": "5.0.0",
92 | "import-sort-parser-typescript": "5.0.0",
93 | "import-sort-style-module-alias": "1.0.4",
94 | "jest": "^26.6.3",
95 | "prettier": "^2.2.1",
96 | "supertest": "^6.1.3",
97 | "terser-webpack-plugin": "^5.1.2",
98 | "ts-jest": "^26.5.4",
99 | "ts-loader": "^8.0.18",
100 | "ts-node": "^9.1.1",
101 | "tsconfig-paths-webpack-plugin": "^3.5.1",
102 | "typescript": "^4.2.3",
103 | "webpack": "^5.37.1",
104 | "webpack-cli": "^4.7.0",
105 | "webpack-ignore-dynamic-require": "^1.0.0"
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/pm2.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps: [
3 | {
4 | name: 'cli-app',
5 | script: './bundle.js',
6 | },
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/tools/gulp/function.ts:
--------------------------------------------------------------------------------
1 | const requiredPackages = ['log4js', 'config', 'toml'];
2 | export function getDeployPackageJson(pkgName?: string): string {
3 | const pkg = require('../../package.json');
4 | const dependencies = {} as any;
5 | Object.keys(pkg.dependencies)
6 | .filter((key) => requiredPackages.includes(key))
7 | .forEach((key) => (dependencies[key] = pkg.dependencies[key]));
8 | const json = {
9 | name: pkgName,
10 | version: pkg.version,
11 | description: pkg.description,
12 | author: pkg.author,
13 | scripts: {
14 | start: 'pm2 start pm2.config.js',
15 | stop: 'pm2 stop pm2.config.js',
16 | restart: 'pm2 restart pm2.config.js',
17 | reload: 'pm2 restart pm2.config.js',
18 | delete: 'pm2 restart pm2.config.js',
19 | 'docker:up': 'docker-compose -f ./docker-compose.yml up -d',
20 | 'docker:down': 'docker-compose -f ./docker-compose.yml down',
21 | },
22 | dependencies,
23 | };
24 |
25 | return JSON.stringify(json, null, 2);
26 | }
27 |
--------------------------------------------------------------------------------
/tools/gulp/gulpfile.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as gulp from 'gulp';
3 | import { getDeployPackageJson } from './function';
4 |
5 | const terser = require('gulp-terser');
6 | const alias = require('gulp-ts-alias');
7 | const ts = require('gulp-typescript');
8 | const gzip = require('gulp-gzip');
9 | const tar = require('gulp-tar');
10 | const zip = require('gulp-zip');
11 |
12 | const util = require('util');
13 |
14 | const exec = util.promisify(require('child_process').exec);
15 |
16 | // read version property from package.json
17 | const packageInfo = require('../../package.json');
18 | const version = packageInfo.version;
19 |
20 | const cliAppName = 'triangular-arbitrage2';
21 | const proCliAppName = 'triangular-arbitrage2-pro';
22 |
23 | function bundleApp(name: string, dirName: string) {
24 | const rootDir = './dist';
25 | const bundleDir = `${rootDir}/${dirName}`;
26 |
27 | if (!fs.existsSync(bundleDir)) {
28 | fs.mkdirSync(bundleDir);
29 | }
30 | fs.copyFileSync(`${rootDir}/${name}-bundle.js`, `${bundleDir}/bundle.js`);
31 | fs.copyFileSync(`./docker-compose.yml`, `${bundleDir}/docker-compose.yml`);
32 | fs.copyFileSync(`./pm2.config.js`, `${bundleDir}/pm2.config.js`);
33 | fs.writeFileSync(`${bundleDir}/package.json`, getDeployPackageJson(dirName));
34 |
35 | const configDir = `${bundleDir}/config`;
36 | if (!fs.existsSync(configDir)) {
37 | fs.mkdirSync(configDir);
38 | }
39 | fs.copyFileSync(`./config/default.sample.toml`, `${configDir}/default.toml`);
40 | }
41 |
42 | gulp.task('build', async (cb: Function) => {
43 | await exec(`yarn webpack:build name=cli-app`);
44 | await exec(`yarn webpack:build name=pro-cli-app`);
45 | cb();
46 | });
47 |
48 | gulp.task('bundle', (cb: Function) => {
49 | bundleApp('cli-app', cliAppName);
50 | bundleApp('pro-cli-app', proCliAppName);
51 | cb();
52 | });
53 |
54 | const terserOptions = {
55 | ecma: 5,
56 | parse: {},
57 | compress: {},
58 | mangle: {
59 | keep_fnames: false,
60 | keep_classnames: false,
61 | properties: false, // Note `mangle.properties` is `false` by default.
62 | },
63 | module: false,
64 | // Deprecated
65 | output: null,
66 | format: { comments: false, beautify: false },
67 | toplevel: false,
68 | nameCache: null,
69 | ie8: false,
70 | keep_classnames: false,
71 | keep_fnames: false,
72 | safari10: false,
73 | };
74 |
75 | gulp.task('minify', () => {
76 | const devProject = ts.createProject('tsconfig.json');
77 | const buildProject = ts.createProject('tsconfig.build.json');
78 | const devConfig = devProject.config;
79 | const buildConfig = buildProject.config;
80 |
81 | const tsconfig = {
82 | compilerOptions: {
83 | ...devConfig.compilerOptions,
84 | ...buildConfig.compilerOptions,
85 | },
86 | exclude: buildConfig.exclude,
87 | compileOnSave: buildConfig.compileOnSave,
88 | };
89 |
90 | const minify = buildProject
91 | .src()
92 | .pipe(alias({ configuration: tsconfig }))
93 | .pipe(buildProject())
94 | .js.pipe(terser(terserOptions))
95 | .pipe(gulp.dest('dist'));
96 |
97 | return minify;
98 | });
99 |
100 | gulp.task('gzip:cli', () => {
101 | return gulp
102 | .src(`./dist/${cliAppName}/**`)
103 | .pipe(tar(`${cliAppName}-v${version}.tar`))
104 | .pipe(gzip())
105 | .pipe(gulp.dest('./dist'));
106 | });
107 |
108 | gulp.task('gzip:pro-cli', () => {
109 | return gulp
110 | .src(`./dist/${proCliAppName}/**`)
111 | .pipe(tar(`${proCliAppName}-v${version}.tar`))
112 | .pipe(gzip())
113 | .pipe(gulp.dest('./dist'));
114 | });
115 |
116 | gulp.task('gzip', gulp.series('gzip:cli', 'gzip:pro-cli'));
117 |
118 | gulp.task('zip:cli', () => {
119 | return gulp
120 | .src(`./dist/${cliAppName}/**`)
121 | .pipe(zip(`${cliAppName}-v${version}.zip`))
122 | .pipe(gulp.dest('./dist'));
123 | });
124 |
125 | gulp.task('zip:pro-cli', () => {
126 | return gulp
127 | .src(`./dist/${proCliAppName}/**`)
128 | .pipe(zip(`${proCliAppName}-v${version}.zip`))
129 | .pipe(gulp.dest('./dist'));
130 | });
131 |
132 | gulp.task('zip', gulp.series('zip:cli', 'zip:pro-cli'));
133 |
--------------------------------------------------------------------------------
/tools/gulp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "noUnusedParameters": false,
5 | "noUnusedLocals": false,
6 | "lib": ["es2015", "es2016"],
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "outDir": ".",
10 | "strict": true,
11 | "target": "es5",
12 | "types": [
13 | "node"
14 | ],
15 | "baseUrl": "."
16 | },
17 | "files": [
18 | "gulpfile.ts"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "watch": false,
5 | "declaration": false,
6 | "declarationMap": false,
7 | "sourceMap": false
8 | },
9 | "exclude": ["node_modules", "dist", "**/*spec.ts", "tools"]
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es2017",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "baseUrl": "./",
12 | "noEmit": false,
13 | "incremental": false,
14 | "paths": {
15 | "@ta2-libs/*": [
16 | "libs/*"
17 | ]
18 | }
19 | },
20 | "include": ["apps/**/*", "libs/**/*"],
21 | "exclude": [
22 | "node_modules",
23 | "dist",
24 | "tools"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/webpack/deploy.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | const { NODE_ENV = 'production' } = process.env;
5 |
6 | console.log(`-- Webpack <${NODE_ENV}> build for Gulp scripts --`);
7 |
8 | module.exports = (env, argv) => {
9 | return {
10 | target: 'node',
11 | mode: NODE_ENV,
12 | entry: `./dist/apps/${env.name}/src/main.js`,
13 | optimization: {
14 | minimize: false,
15 | removeAvailableModules: false,
16 | removeEmptyChunks: false,
17 | splitChunks: false,
18 | },
19 | output: {
20 | // Remember that this file is in a subdirectory, so the output should be in the dist/
21 | // directory of the project root
22 | path: path.resolve(__dirname, '../dist'),
23 | filename: `${env.name}-bundle.js`,
24 | },
25 | plugins: [
26 | new webpack.IgnorePlugin({
27 | /**
28 | * There is a small problem with Nest's idea of lazy require() calls,
29 | * Webpack tries to load these lazy imports that you may not be using,
30 | * so we must explicitly handle the issue.
31 | * Refer to: https://github.com/nestjs/nest/issues/1706
32 | */
33 | checkResource(resource) {
34 | const lazyImports = [
35 | '@nestjs/websockets/socket-modules',
36 | '@nestjs/microservices/microservices-module',
37 | '@nestjs/microservices',
38 | '@nestjs/platform-express',
39 | 'cache-manager',
40 | 'class-validator',
41 | 'class-transformer',
42 | ];
43 | if (!lazyImports.includes(resource)) {
44 | return false;
45 | }
46 | try {
47 | require.resolve(resource);
48 | } catch (err) {
49 | return true;
50 | }
51 | return false;
52 | },
53 | }),
54 | ],
55 | stats: {
56 | // This is optional, but it hides noisey warnings
57 | warningsFilter: [
58 | 'node_modules/@nestjs/common/utils/load-package.utils.js',
59 | 'node_modules/@nestjs/core/helpers/load-adapter.js',
60 | 'node_modules/@nestjs/core/helpers/optional-require.js',
61 | 'node_modules/express/lib/view.js',
62 | 'node_modules/binance/node_modules/ws/lib/BufferUtil.js',
63 | 'node_modules/binance/node_modules/ws/lib/Validation.js',
64 | (warning) => false,
65 | ],
66 | },
67 | externals: [
68 | {
69 | '@nestjs/websockets/socket-module': 'commonjs2 @nestjs/websockets/socket-module',
70 | '@nestjs/microservices/microservices-module': 'commonjs2 @nestjs/microservices/microservices-module',
71 | /* binance: 'commonjs2 binance',*/
72 | log4js: 'commonjs2 log4js',
73 | config: 'commonjs2 config',
74 | toml: 'commonjs2 toml',
75 | },
76 | ],
77 | };
78 | };
79 |
--------------------------------------------------------------------------------
/webpack/migrations.config.js:
--------------------------------------------------------------------------------
1 | const glob = require('glob');
2 | const path = require('path');
3 | // TypeScript编译选项
4 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
5 | // Minimization option
6 | const TerserPlugin = require('terser-webpack-plugin');
7 |
8 | const { NODE_ENV = 'production' } = process.env;
9 |
10 | console.log(`-- Webpack <${NODE_ENV}> build for migrations scripts --`);
11 |
12 | module.exports = {
13 | target: 'node',
14 | mode: NODE_ENV,
15 | // Dynamically generate a `{ [name]: sourceFileName }` map for the `entry` option
16 | // change `src/db/migrations` to the relative path to your migration folder
17 | entry: glob.sync(path.resolve('src/migration/*.ts')).reduce((entries, filename) => {
18 | const migrationName = path.basename(filename, '.ts');
19 | return Object.assign({}, entries, {
20 | [migrationName]: filename,
21 | });
22 | }, {}),
23 | resolve: {
24 | // assuming all your migration files are written in TypeScript
25 | extensions: ['.ts'],
26 | // Use the same configuration as NestJS
27 | plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.build.json' })],
28 | },
29 | module: {
30 | rules: [{ test: /\.ts$/, loader: 'ts-loader' }],
31 | },
32 | output: {
33 | // Remember that this file is in a subdirectory, so the output should be in the dist/
34 | // directory of the project root
35 | path: __dirname + '/../dist/migration',
36 | // this is important - we want UMD (Universal Module Definition) for migration files.
37 | libraryTarget: 'umd',
38 | filename: '[name].js',
39 | },
40 | optimization: {
41 | minimizer: [
42 | // Migrations rely on class and function names, so keep them.
43 | new TerserPlugin({
44 | terserOptions: {
45 | mangle: true, // Note `mangle.properties` is `false` by default.
46 | keep_classnames: true,
47 | keep_fnames: true,
48 | },
49 | }),
50 | ],
51 | },
52 | };
53 | 之后;
54 |
--------------------------------------------------------------------------------
/webpack/typeorm-cli.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | // TypeScript compilation option
3 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
4 | // Don't try to replace require calls to dynamic files
5 | const IgnoreDynamicRequire = require('webpack-ignore-dynamic-require');
6 |
7 | const { NODE_ENV = 'production' } = process.env;
8 |
9 | console.log(`-- Webpack <${NODE_ENV}> build for TypeORM CLI --`);
10 |
11 | module.exports = {
12 | target: 'node',
13 | mode: NODE_ENV,
14 | entry: './node_modules/typeorm/cli.js',
15 | output: {
16 | // Remember that this file is in a subdirectory, so the output should be in the dist/
17 | // directory of the project root
18 | path: path.resolve(__dirname, '../dist'),
19 | filename: 'migration.js',
20 | },
21 | resolve: {
22 | extensions: ['.ts', '.js'],
23 | // Use the same configuration as NestJS
24 | plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.build.json' })],
25 | },
26 | module: {
27 | rules: [
28 | { test: /\.ts$/, loader: 'ts-loader' },
29 | // Skip the shebang of typeorm/cli.js
30 | { test: /\.[tj]s$/i, loader: 'shebang-loader' },
31 | ],
32 | },
33 | externals: [
34 | {
35 | // I'll skip pg-native in the production deployement, and use the pure JS implementation
36 | 'pg-native': 'commonjs2 pg-native',
37 | },
38 | ],
39 | plugins: [
40 | // Let NodeJS handle are requires that can't be resolved at build time
41 | new IgnoreDynamicRequire(),
42 | ],
43 | };
44 |
--------------------------------------------------------------------------------