├── .gitignore ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── ecosystem.config.js ├── jest.config.ts ├── jest.setup.ts ├── package.json ├── scripts ├── deploy.sh ├── logs.sh ├── snapshot.ts ├── ssh.sh └── zIndex.ts ├── src ├── __fixtures__ │ └── orders.ts ├── api │ ├── binance.ts │ ├── database.ts │ └── slack.ts ├── binance-connector.d.ts ├── config.ts ├── index.ts └── strategy │ ├── falling.ts │ ├── first.spec.ts │ ├── first.ts │ ├── strategy.config.ts │ ├── strategy.ts │ ├── updateEarningsSnapshot.ts │ └── wallet.ts ├── tsconfig.json ├── tsup.config.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.19.0](https://github.com/arantespp/cryptobot/compare/v1.18.0...v1.19.0) (2022-01-08) 6 | 7 | 8 | ### Features 9 | 10 | * update message ([5220b30](https://github.com/arantespp/cryptobot/commit/5220b3056ed00b0bc36009b59d039f72cfd11606)) 11 | 12 | ## [1.18.0](https://github.com/arantespp/cryptobot/compare/v1.17.8...v1.18.0) (2022-01-08) 13 | 14 | 15 | ### Features 16 | 17 | * update message ([f106ff7](https://github.com/arantespp/cryptobot/commit/f106ff741569242e9456ec275e79a9ecce5a32d4)) 18 | 19 | ### [1.17.8](https://github.com/arantespp/cryptobot/compare/v1.17.7...v1.17.8) (2021-12-19) 20 | 21 | ### [1.17.7](https://github.com/arantespp/cryptobot/compare/v1.17.6...v1.17.7) (2021-12-15) 22 | 23 | ### [1.17.6](https://github.com/arantespp/cryptobot/compare/v1.17.5...v1.17.6) (2021-12-15) 24 | 25 | ### [1.17.5](https://github.com/arantespp/cryptobot/compare/v1.17.4...v1.17.5) (2021-12-15) 26 | 27 | ### [1.17.4](https://github.com/arantespp/cryptobot/compare/v1.17.3...v1.17.4) (2021-12-15) 28 | 29 | ### [1.17.3](https://github.com/arantespp/cryptobot/compare/v1.17.2...v1.17.3) (2021-12-15) 30 | 31 | ### [1.17.2](https://github.com/arantespp/cryptobot/compare/v1.17.1...v1.17.2) (2021-12-15) 32 | 33 | ### [1.17.1](https://github.com/arantespp/cryptobot/compare/v1.17.0...v1.17.1) (2021-12-15) 34 | 35 | ## [1.17.0](https://github.com/arantespp/cryptobot/compare/v1.16.5...v1.17.0) (2021-12-15) 36 | 37 | 38 | ### Features 39 | 40 | * update wallet ([a2d1953](https://github.com/arantespp/cryptobot/commit/a2d19530fcadf887c7a18698a82be774360d6937)) 41 | 42 | ### [1.16.5](https://github.com/arantespp/cryptobot/compare/v1.16.4...v1.16.5) (2021-12-08) 43 | 44 | ### [1.16.4](https://github.com/arantespp/cryptobot/compare/v1.16.3...v1.16.4) (2021-12-07) 45 | 46 | ### [1.16.3](https://github.com/arantespp/cryptobot/compare/v1.16.2...v1.16.3) (2021-12-07) 47 | 48 | ### [1.16.2](https://github.com/arantespp/cryptobot/compare/v1.16.1...v1.16.2) (2021-12-07) 49 | 50 | ### [1.16.1](https://github.com/arantespp/cryptobot/compare/v1.16.0...v1.16.1) (2021-12-07) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * slack logs every hour ([90741be](https://github.com/arantespp/cryptobot/commit/90741beb9eb52abb7fe9eeb3b0c3dad2ccf8fcbe)) 56 | 57 | ## [1.16.0](https://github.com/arantespp/cryptobot/compare/v1.15.3...v1.16.0) (2021-12-06) 58 | 59 | 60 | ### Features 61 | 62 | * add slack logs ([8ce8dea](https://github.com/arantespp/cryptobot/commit/8ce8dea37381dc2e77b7ea93f27b8b1d32d217a7)) 63 | 64 | ### [1.15.3](https://github.com/arantespp/cryptobot/compare/v1.15.2...v1.15.3) (2021-12-03) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * updte wallet ([f079696](https://github.com/arantespp/cryptobot/commit/f079696e7b172a2fb87433eac0cc8b5ef4171735)) 70 | 71 | ### [1.15.2](https://github.com/arantespp/cryptobot/compare/v1.15.1...v1.15.2) (2021-12-03) 72 | 73 | ### [1.15.1](https://github.com/arantespp/cryptobot/compare/v1.15.0...v1.15.1) (2021-12-02) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * LOWEST_QUANTITY_ASSETS_TO_NOT_TRADE ([5460624](https://github.com/arantespp/cryptobot/commit/5460624cb5c70f6d8aca8eebc8c1905cb330aff5)) 79 | 80 | ## [1.15.0](https://github.com/arantespp/cryptobot/compare/v1.14.0...v1.15.0) (2021-12-02) 81 | 82 | 83 | ### Features 84 | 85 | * update strategy - sell item if z-score is too high ([fcd4e3b](https://github.com/arantespp/cryptobot/commit/fcd4e3be0e19910926c64d07588e4d0e8de99bc3)) 86 | 87 | ## [1.14.0](https://github.com/arantespp/cryptobot/compare/v1.13.1...v1.14.0) (2021-11-29) 88 | 89 | 90 | ### Features 91 | 92 | * update wallet ([2968f36](https://github.com/arantespp/cryptobot/commit/2968f3690e513a5d3d92f60279c7b7a71d5cfda1)) 93 | 94 | ### [1.13.1](https://github.com/arantespp/cryptobot/compare/v1.13.0...v1.13.1) (2021-11-26) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * min notional to trade ([fbf53a1](https://github.com/arantespp/cryptobot/commit/fbf53a1fb7e574afe3ace6591242ccc132d16f6a)) 100 | 101 | ## [1.13.0](https://github.com/arantespp/cryptobot/compare/v1.12.10...v1.13.0) (2021-11-26) 102 | 103 | 104 | ### Features 105 | 106 | * add LINK ([69890cc](https://github.com/arantespp/cryptobot/commit/69890cc080de51140380a75c0b5e57036ebd5b1a)) 107 | 108 | ### [1.12.10](https://github.com/arantespp/cryptobot/compare/v1.12.9...v1.12.10) (2021-11-25) 109 | 110 | ### [1.12.9](https://github.com/arantespp/cryptobot/compare/v1.12.8...v1.12.9) (2021-11-25) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * filter lowest assets to trade ([e970f90](https://github.com/arantespp/cryptobot/commit/e970f9090ba58a271ef4afde51e711522d147526)) 116 | 117 | ### [1.12.8](https://github.com/arantespp/cryptobot/compare/v1.12.7...v1.12.8) (2021-11-25) 118 | 119 | ### [1.12.7](https://github.com/arantespp/cryptobot/compare/v1.12.6...v1.12.7) (2021-11-25) 120 | 121 | ### [1.12.6](https://github.com/arantespp/cryptobot/compare/v1.12.5...v1.12.6) (2021-11-25) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * second buy order asset ([f609fc6](https://github.com/arantespp/cryptobot/commit/f609fc6ca6febbb50fa9a854c413eebabca2f1d2)) 127 | 128 | ### [1.12.5](https://github.com/arantespp/cryptobot/compare/v1.12.4...v1.12.5) (2021-11-25) 129 | 130 | ### [1.12.4](https://github.com/arantespp/cryptobot/compare/v1.12.3...v1.12.4) (2021-11-25) 131 | 132 | ### [1.12.3](https://github.com/arantespp/cryptobot/compare/v1.12.2...v1.12.3) (2021-11-25) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * update strategy data ([d9506d2](https://github.com/arantespp/cryptobot/commit/d9506d23712343fca382a7125eeafb45f78b84b6)) 138 | 139 | ### [1.12.2](https://github.com/arantespp/cryptobot/compare/v1.12.1...v1.12.2) (2021-11-25) 140 | 141 | ### [1.12.1](https://github.com/arantespp/cryptobot/compare/v1.12.0...v1.12.1) (2021-11-25) 142 | 143 | ## [1.12.0](https://github.com/arantespp/cryptobot/compare/v1.11.3...v1.12.0) (2021-11-25) 144 | 145 | 146 | ### Features 147 | 148 | * add quote operation after asset operation ([b4510d6](https://github.com/arantespp/cryptobot/commit/b4510d68716d2f281de784e370dfb3fa35d30322)) 149 | 150 | ### [1.11.3](https://github.com/arantespp/cryptobot/compare/v1.11.2...v1.11.3) (2021-11-24) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * profit valuation ([482adf4](https://github.com/arantespp/cryptobot/commit/482adf49785d3e64b0aff59ca0d37400949ee6ca)) 156 | 157 | ### [1.11.2](https://github.com/arantespp/cryptobot/compare/v1.11.1...v1.11.2) (2021-11-24) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * min notional to apply strategy ([dde6790](https://github.com/arantespp/cryptobot/commit/dde6790f79486c249ca3f726492ba6f0e4373f83)) 163 | 164 | ### [1.11.1](https://github.com/arantespp/cryptobot/compare/v1.11.0...v1.11.1) (2021-11-24) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * update slack messages format ([6f59a75](https://github.com/arantespp/cryptobot/commit/6f59a75395006299c7a8166d861ff5fb253de39b)) 170 | 171 | ## [1.11.0](https://github.com/arantespp/cryptobot/compare/v1.10.0...v1.11.0) (2021-11-23) 172 | 173 | 174 | ### Features 175 | 176 | * add slack messages ([a34b010](https://github.com/arantespp/cryptobot/commit/a34b01073f40eea8c901ac18b58254b6a52d23ae)) 177 | 178 | ## [1.10.0](https://github.com/arantespp/cryptobot/compare/v1.9.0...v1.10.0) (2021-11-23) 179 | 180 | 181 | ### Features 182 | 183 | * add AVAX, DOGE, and DOT ([5866dda](https://github.com/arantespp/cryptobot/commit/5866ddafac000daa00f73e95e5d8e4f6265d4aa6)) 184 | 185 | ## [1.9.0](https://github.com/arantespp/cryptobot/compare/v1.8.2...v1.9.0) (2021-11-23) 186 | 187 | 188 | ### Features 189 | 190 | * update profit min percentage ([15e53d8](https://github.com/arantespp/cryptobot/commit/15e53d8ba063ca510a7982799f0b115356f488b2)) 191 | 192 | ### [1.8.2](https://github.com/arantespp/cryptobot/compare/v1.8.1...v1.8.2) (2021-11-23) 193 | 194 | 195 | ### Bug Fixes 196 | 197 | * query returning the highest price ;/ ([2e405b0](https://github.com/arantespp/cryptobot/commit/2e405b00e1745c16dc45d7da0c32a4ebfb99174d)) 198 | 199 | ### [1.8.1](https://github.com/arantespp/cryptobot/compare/v1.8.0...v1.8.1) (2021-11-21) 200 | 201 | ## [1.8.0](https://github.com/arantespp/cryptobot/compare/v1.7.2...v1.8.0) (2021-11-21) 202 | 203 | 204 | ### Features 205 | 206 | * add diff to snapshot ([2b7605a](https://github.com/arantespp/cryptobot/commit/2b7605a56ec25faeb6c3d7436948a60b1eb34c7b)) 207 | 208 | ### [1.7.2](https://github.com/arantespp/cryptobot/compare/v1.7.1...v1.7.2) (2021-11-21) 209 | 210 | 211 | ### Bug Fixes 212 | 213 | * tests ([e5eec29](https://github.com/arantespp/cryptobot/commit/e5eec29bb73c1ee509590169e004047ec59efafd)) 214 | 215 | ### [1.7.1](https://github.com/arantespp/cryptobot/compare/v1.7.0...v1.7.1) (2021-11-21) 216 | 217 | 218 | ### Bug Fixes 219 | 220 | * add tests before deploy ([2565377](https://github.com/arantespp/cryptobot/commit/25653774433aa9cd1d2c9f0186dfed18aff27638)) 221 | 222 | ## [1.7.0](https://github.com/arantespp/cryptobot/compare/v1.6.0...v1.7.0) (2021-11-21) 223 | 224 | 225 | ### Features 226 | 227 | * add earnings snapshot ([6223c43](https://github.com/arantespp/cryptobot/commit/6223c43a039b5b7bf97b72969376d3f622da4e67)) 228 | 229 | ## [1.6.0](https://github.com/arantespp/cryptobot/compare/v1.5.2...v1.6.0) (2021-11-21) 230 | 231 | 232 | ### Features 233 | 234 | * add min notional filter ([5b7680d](https://github.com/arantespp/cryptobot/commit/5b7680da02c85d0b3829ae08a7578e3c4d3e91b2)) 235 | 236 | ### [1.5.2](https://github.com/arantespp/cryptobot/compare/v1.5.1...v1.5.2) (2021-11-21) 237 | 238 | ### [1.5.1](https://github.com/arantespp/cryptobot/compare/v1.5.0...v1.5.1) (2021-11-21) 239 | 240 | 241 | ### Bug Fixes 242 | 243 | * filter lowest buys by ration ([92269fe](https://github.com/arantespp/cryptobot/commit/92269fe3fc257559fd3cfa033fac2979679b00fe)) 244 | 245 | ## [1.5.0](https://github.com/arantespp/cryptobot/compare/v1.4.0...v1.5.0) (2021-11-21) 246 | 247 | 248 | ### Features 249 | 250 | * add SHIB XLM and FTM ([586e2a8](https://github.com/arantespp/cryptobot/commit/586e2a8773768f51c68f3e7f43120b31a019efc1)) 251 | 252 | ## [1.4.0](https://github.com/arantespp/cryptobot/compare/v1.3.0...v1.4.0) (2021-11-21) 253 | 254 | 255 | ### Features 256 | 257 | * add vet and matic ([faa70a7](https://github.com/arantespp/cryptobot/commit/faa70a7f81436fafec7c1697088f0458503b60b7)) 258 | 259 | ## [1.3.0](https://github.com/arantespp/cryptobot/compare/v1.2.0...v1.3.0) (2021-11-21) 260 | 261 | 262 | ### Features 263 | 264 | * add deploy script ([27ca0ea](https://github.com/arantespp/cryptobot/commit/27ca0ea9ac88eefd48e08cce9b492864ec5b12fd)) 265 | 266 | ## [1.2.0](https://github.com/arantespp/cryptobot/compare/v1.1.0...v1.2.0) (2021-11-21) 267 | 268 | 269 | ### Features 270 | 271 | * add asset earnings ([2ec53a0](https://github.com/arantespp/cryptobot/commit/2ec53a0ccf90c1e107e40068289df76480b6d9ed)) 272 | * finished sell orders ([b50c886](https://github.com/arantespp/cryptobot/commit/b50c8860cc4b4e21da38752b12e9b5895b1f9ae2)) 273 | 274 | 275 | ### Bug Fixes 276 | 277 | * debug message ([0752103](https://github.com/arantespp/cryptobot/commit/0752103f1db43f4f1c39f35261422510ae5da72c)) 278 | * order and asset args inconsistency ([1f8a12e](https://github.com/arantespp/cryptobot/commit/1f8a12e889a503df47c2a90042ec224620bfafb9)) 279 | * remove unused variable ([e10d692](https://github.com/arantespp/cryptobot/commit/e10d6923c0a81d1dc8e87e2f4b472b34120f8dd6)) 280 | 281 | ## 1.1.0 (2021-11-21) 282 | 283 | 284 | ### Features 285 | 286 | * add min notional funcionality ([fc9ca67](https://github.com/arantespp/cryptobot/commit/fc9ca67f8fc2ec186d12fbc90a42004ac4ceff13)) 287 | * add pm2 and tsup ([372f222](https://github.com/arantespp/cryptobot/commit/372f222b88e50028c92ce9020dadb4240b16e5a7)) 288 | * add quote balance ([bd57dcc](https://github.com/arantespp/cryptobot/commit/bd57dcc0379197590751f60c66686763951303ef)) 289 | * add quote operation ([07c2697](https://github.com/arantespp/cryptobot/commit/07c26971dae9b18757d05a58bab0a74968b28ff1)) 290 | * add save buy order ([b394def](https://github.com/arantespp/cryptobot/commit/b394def5b3f2a3675af771aeb7c2c4f04ef0d664)) 291 | * create strucuture for assets operation ([3210250](https://github.com/arantespp/cryptobot/commit/3210250281b0acdcb8d93c4c21f82c17122374e4)) 292 | * first commit ([552c013](https://github.com/arantespp/cryptobot/commit/552c0139ffdb9808ff241ad900ff3cb9806729ff)) 293 | * quote operation update deposits balance ([8382996](https://github.com/arantespp/cryptobot/commit/83829962ce741e406e64f50efab5fb80adf95ec3)) 294 | * sell order first implementation ([6cec8c0](https://github.com/arantespp/cryptobot/commit/6cec8c053cab187de1a00af1fd937026585a7811)) 295 | 296 | 297 | ### Bug Fixes 298 | 299 | * update comission ([0a3fad8](https://github.com/arantespp/cryptobot/commit/0a3fad8bc5ef3fb9c16335363fa1ef95b3ee12aa)) 300 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CryptoBot 2 | 3 | ## Questions 4 | 5 | ### How to commit and deploy? 6 | 7 | 1. Commit the changes to your local repository. 8 | 9 | 2. Run `yarn deploy`. 10 | 11 | ### How to use production CryptoBot? 12 | 13 | 1. Go to Binance and allow IP for CryptoBot. 14 | 15 | 2. Run `NODE_ENV=production yarn start` 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'CryptoBot', 5 | script: 'dist/index.js', 6 | watch: ['dist/index.js'], 7 | watch_delay: 30 * 1000, 8 | time: true, 9 | env_production: { 10 | NODE_ENV: 'production', 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/tmp/jest_rt", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: 'coverage', 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: 'v8', 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | setupFiles: ['./jest.setup.ts'], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jest-circus/runner", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | process.env.BINANCE_API_KEY = 'BINANCE_API_KEY'; 2 | process.env.BINANCE_SECRET_KEY = 'BINANCE_SECRET_KEY'; 3 | process.env.AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID'; 4 | process.env.AWS_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY'; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cryptobot", 3 | "version": "1.19.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "tsup", 9 | "release": "standard-version && git push --follow-tags origin main", 10 | "deploy": "sh ./scripts/deploy.sh", 11 | "start": "ts-node src/index.ts", 12 | "start:production": "pm2 start ecosystem.config.js --env production", 13 | "ssh": "sh scripts/ssh.sh", 14 | "logs": "sh scripts/logs.sh", 15 | "snapshot": "ts-node scripts/snapshot.ts", 16 | "z-index": "ts-node scripts/zIndex.ts" 17 | }, 18 | "keywords": [], 19 | "author": "Pedro Arantes", 20 | "license": "UNLICENSED", 21 | "dependencies": { 22 | "@aws-sdk/client-dynamodb": "^3.41.0", 23 | "@aws-sdk/util-dynamodb": "^3.42.0", 24 | "@binance/connector": "^1.5.0", 25 | "@slack/webhook": "^6.0.0", 26 | "date-fns": "^2.26.0", 27 | "debug": "^4.3.2", 28 | "decimal.js": "^10.3.1", 29 | "deepmerge": "^4.2.2", 30 | "dotenv": "^10.0.0", 31 | "mathjs": "^10.0.0", 32 | "node-cron": "^3.0.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.16.0", 36 | "@babel/preset-env": "^7.16.4", 37 | "@babel/preset-typescript": "^7.16.0", 38 | "@types/jest": "^27.0.3", 39 | "@types/node": "^16.11.8", 40 | "@types/node-cron": "^3.0.0", 41 | "babel-jest": "^27.3.1", 42 | "jest": "^27.3.1", 43 | "standard-version": "^9.3.2", 44 | "ts-node": "^10.4.0", 45 | "tsup": "^5.7.4", 46 | "typescript": "^4.5.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | SERVER_IP=54.161.159.172 4 | 5 | yarn run test 6 | yarn run build 7 | yarn run release 8 | scp -i ~/.ssh/arantespp-personal-aws.pem -r ./dist/* ubuntu@$SERVER_IP:~/cryptobot/dist/ 9 | scp -i ~/.ssh/arantespp-personal-aws.pem -r ./src/* ubuntu@$SERVER_IP:~/cryptobot/src/ 10 | scp -i ~/.ssh/arantespp-personal-aws.pem -r ./scripts/* ubuntu@$SERVER_IP:~/cryptobot/scripts/ 11 | scp -i ~/.ssh/arantespp-personal-aws.pem package.json .env.production ecosystem.config.js tsconfig.json ubuntu@$SERVER_IP:~/cryptobot -------------------------------------------------------------------------------- /scripts/logs.sh: -------------------------------------------------------------------------------- 1 | scp -i ~/.ssh/arantespp-personal-aws.pem -r ubuntu@54.161.159.172:~/.pm2/logs ./logs 2 | -------------------------------------------------------------------------------- /scripts/snapshot.ts: -------------------------------------------------------------------------------- 1 | import '../src/config'; 2 | 3 | import { updateEarningsSnapshot } from '../src/strategy/updateEarningsSnapshot'; 4 | 5 | updateEarningsSnapshot().then(console.log).catch(console.error); 6 | -------------------------------------------------------------------------------- /scripts/ssh.sh: -------------------------------------------------------------------------------- 1 | ssh -i ~/.ssh/arantespp-personal-aws.pem ubuntu@54.161.159.172 -------------------------------------------------------------------------------- /scripts/zIndex.ts: -------------------------------------------------------------------------------- 1 | import '../src/config'; 2 | 3 | import { getStrategyData } from '../src/api/binance'; 4 | import { 5 | getWalletProportionTickers, 6 | getExtremeProportions, 7 | getWalletProportion, 8 | } from '../src/strategy/first'; 9 | 10 | (async () => { 11 | const walletProportion = await getWalletProportion(); 12 | 13 | const tickers = getWalletProportionTickers(walletProportion); 14 | 15 | const strategyData = await getStrategyData(tickers); 16 | 17 | const extremeProportions = getExtremeProportions({ 18 | strategyData, 19 | walletProportion, 20 | }); 21 | 22 | console.log(extremeProportions); 23 | })(); 24 | -------------------------------------------------------------------------------- /src/__fixtures__/orders.ts: -------------------------------------------------------------------------------- 1 | export const buy = { 2 | symbol: 'BTCBUSD', 3 | orderId: 3753014780, 4 | orderListId: -1, 5 | clientOrderId: '8eBjTVyDVQshaGeIAtef4o', 6 | transactTime: 1637405066716, 7 | price: '0.00000000', 8 | origQty: '0.00034000', 9 | executedQty: '0.00034000', 10 | cummulativeQuoteQty: '19.93200700', 11 | status: 'FILLED', 12 | timeInForce: 'GTC', 13 | type: 'MARKET', 14 | side: 'BUY', 15 | fills: [ 16 | { 17 | price: '58623.55000000', 18 | qty: '0.00034000', 19 | commission: '0.00000034', 20 | commissionAsset: 'BTC', 21 | tradeId: 269707612, 22 | }, 23 | ], 24 | }; 25 | 26 | export const sell = { 27 | symbol: 'BTCBUSD', 28 | orderId: 3753303797, 29 | orderListId: -1, 30 | clientOrderId: 'us3l9KE5rJnYhgmUois4mj', 31 | transactTime: 1637409784301, 32 | price: '0.00000000', 33 | origQty: '0.00034000', 34 | executedQty: '0.00034000', 35 | cummulativeQuoteQty: '19.95469180', 36 | status: 'FILLED', 37 | timeInForce: 'GTC', 38 | type: 'MARKET', 39 | side: 'SELL', 40 | fills: [ 41 | { 42 | price: '58690.27000000', 43 | qty: '0.00034000', 44 | commission: '0.01995469', 45 | commissionAsset: 'BUSD', 46 | tradeId: 269723208, 47 | }, 48 | ], 49 | }; 50 | -------------------------------------------------------------------------------- /src/api/binance.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://binance.github.io/binance-connector-node/module-Market.html 3 | */ 4 | import { Spot, ExchangeInfo } from '@binance/connector'; 5 | import merge from 'deepmerge'; 6 | 7 | const { BINANCE_API_KEY, BINANCE_SECRET_KEY, BINANCE_BASE_URL } = process.env; 8 | 9 | if (!BINANCE_API_KEY) { 10 | throw new Error('BINANCE_API_KEY is not set'); 11 | } 12 | 13 | if (!BINANCE_SECRET_KEY) { 14 | throw new Error('BINANCE_SECRET_KEY is not set'); 15 | } 16 | 17 | export const QUOTE_BASE_TICKER = 'BUSD'; 18 | 19 | const getSymbolFromAsset = (asset: string) => 20 | `${asset}${QUOTE_BASE_TICKER}`.toUpperCase(); 21 | 22 | const getSymbolsFromAssets = (assets: string[]) => 23 | assets.map(getSymbolFromAsset); 24 | 25 | /** 26 | * Strategic Data 27 | */ 28 | const client = new Spot(BINANCE_API_KEY, BINANCE_SECRET_KEY, { 29 | baseURL: BINANCE_BASE_URL, 30 | }); 31 | 32 | let _exchangeInfo: ExchangeInfo; 33 | 34 | export const getExchangeInfo = async () => { 35 | if (!_exchangeInfo) { 36 | /** 37 | * https://binance-docs.github.io/apidocs/spot/en/#exchange-information 38 | */ 39 | _exchangeInfo = (await client.exchangeInfo()).data; 40 | } 41 | 42 | return _exchangeInfo; 43 | }; 44 | 45 | /** 46 | * Return the greatest minNotional value for the given assets. 47 | */ 48 | export const getAssetsMinNotional = async (assets: string[]) => { 49 | const exchangeInfo = await getExchangeInfo(); 50 | 51 | const symbols = getSymbolsFromAssets(assets); 52 | 53 | const minNotional = exchangeInfo.symbols 54 | .filter(({ symbol }) => symbols.includes(symbol)) 55 | .reduce((acc, cur) => { 56 | const filter = cur.filters.find( 57 | ({ filterType }) => filterType === 'MIN_NOTIONAL' 58 | ); 59 | 60 | if (filter?.filterType === 'MIN_NOTIONAL') { 61 | return Math.max(acc, Number(filter.minNotional)); 62 | } 63 | 64 | return acc; 65 | }, 0); 66 | 67 | return minNotional; 68 | }; 69 | 70 | type WalletBalances = { 71 | [ticker: string]: { 72 | quantity: number; 73 | }; 74 | }; 75 | 76 | export const getWalletBalances = async ( 77 | assets: string[] 78 | ): Promise => { 79 | /** 80 | * https://binance-docs.github.io/apidocs/spot/en/#account-information-user_data 81 | */ 82 | const { data } = await client.account(); 83 | 84 | const { balances } = data; 85 | 86 | const assetsBalances = balances.filter( 87 | ({ asset }) => assets.includes(asset) || asset === QUOTE_BASE_TICKER 88 | ); 89 | 90 | return assetsBalances.reduce((acc, cur) => { 91 | acc[cur.asset] = { quantity: Number(cur.free) }; 92 | return acc; 93 | }, {}); 94 | }; 95 | 96 | type AllAssetsPrice = { 97 | [ticker: string]: { 98 | asset: string; 99 | symbol: string; 100 | price: number; 101 | }; 102 | }; 103 | 104 | export const getAllAssetsPrice = async ( 105 | assets: string[] 106 | ): Promise => { 107 | /** 108 | * https://binance-docs.github.io/apidocs/spot/en/#symbol-price-ticker 109 | */ 110 | const tickersPrice = await Promise.all( 111 | assets.map((asset) => 112 | client 113 | .tickerPrice(getSymbolFromAsset(asset)) 114 | .then(({ data }) => ({ ...data, price: Number(data.price), asset })) 115 | ) 116 | ); 117 | 118 | return tickersPrice.reduce((acc, cur) => { 119 | acc[cur.asset] = cur; 120 | return acc; 121 | }, {}); 122 | }; 123 | 124 | export type StrategyData = { 125 | minNotional: number; 126 | assets: { 127 | [asset: string]: AllAssetsPrice[string] & 128 | WalletBalances[string] & { 129 | totalValue: number; 130 | filters?: ExchangeInfo['symbols'][number]['filters']; 131 | }; 132 | }; 133 | }; 134 | 135 | export const getStrategyData = async ( 136 | assets: string[] 137 | ): Promise => { 138 | const [balances, tickersPrice, minNotional, exchangeInfo] = await Promise.all( 139 | [ 140 | getWalletBalances(assets), 141 | getAllAssetsPrice(assets), 142 | getAssetsMinNotional(assets), 143 | getExchangeInfo(), 144 | ] 145 | ); 146 | 147 | const assetsData = merge(balances, tickersPrice) as StrategyData['assets']; 148 | 149 | Object.keys(assetsData).forEach((asset) => { 150 | /** 151 | * Assign totalValue. 152 | */ 153 | if (asset === QUOTE_BASE_TICKER) { 154 | assetsData[asset].totalValue = assetsData[asset].quantity; 155 | } else { 156 | assetsData[asset].totalValue = 157 | assetsData[asset].price * assetsData[asset].quantity; 158 | } 159 | 160 | /** 161 | * Assign filters. 162 | */ 163 | assetsData[asset].filters = exchangeInfo.symbols.find( 164 | ({ symbol }) => symbol === getSymbolFromAsset(asset) 165 | )?.filters; 166 | }); 167 | 168 | return { assets: assetsData, minNotional }; 169 | }; 170 | 171 | /** 172 | * Orders 173 | */ 174 | 175 | export const buyOrder = async ({ 176 | asset, 177 | quoteOrderQty, 178 | }: { 179 | asset: string; 180 | quoteOrderQty: number; 181 | }) => { 182 | const symbol = getSymbolFromAsset(asset); 183 | 184 | const order = await client.newOrder(symbol, 'BUY', 'MARKET', { 185 | quoteOrderQty, 186 | }); 187 | 188 | return order.data; 189 | }; 190 | 191 | export const sellOrder = async ({ 192 | asset, 193 | quantity, 194 | }: { 195 | asset: string; 196 | quantity: number; 197 | }) => { 198 | const symbol = getSymbolFromAsset(asset); 199 | 200 | const order = await client.newOrder(symbol, 'SELL', 'MARKET', { 201 | quantity, 202 | }); 203 | 204 | return order.data; 205 | }; 206 | 207 | export const redeemFlexibleSavings = async ({ 208 | asset, 209 | amount, 210 | }: { 211 | asset: string; 212 | amount: number; 213 | }) => { 214 | const productId = `${asset}001`; 215 | const { data } = await client.savingsFlexibleRedeem( 216 | productId, 217 | amount, 218 | 'FAST' 219 | ); 220 | 221 | return data; 222 | }; 223 | -------------------------------------------------------------------------------- /src/api/database.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamoDBClient, 3 | GetItemCommand, 4 | PutItemCommand, 5 | QueryCommand, 6 | UpdateItemCommand, 7 | } from '@aws-sdk/client-dynamodb'; 8 | import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; 9 | 10 | const { 11 | AWS_REGION, 12 | AWS_ACCESS_KEY_ID, 13 | AWS_SECRET_ACCESS_KEY, 14 | DYNAMODB_TABLE_NAME, 15 | } = process.env; 16 | 17 | if (!AWS_ACCESS_KEY_ID) { 18 | throw new Error('AWS_ACCESS_KEY_ID is not set'); 19 | } 20 | 21 | if (!AWS_SECRET_ACCESS_KEY) { 22 | throw new Error('AWS_SECRET_ACCESS_KEY is not set'); 23 | } 24 | 25 | export const indexes = { 26 | pkStatusIndex: 'pk-status-index', 27 | }; 28 | 29 | const TABLE_NAME = DYNAMODB_TABLE_NAME; 30 | 31 | const dynamoDb = new DynamoDBClient({ 32 | region: AWS_REGION, 33 | credentials: { 34 | accessKeyId: AWS_ACCESS_KEY_ID, 35 | secretAccessKey: AWS_SECRET_ACCESS_KEY, 36 | }, 37 | }); 38 | 39 | type PrimaryKey = { 40 | pk: string; 41 | sk: string; 42 | }; 43 | 44 | type Item = PrimaryKey & { [key: string]: any }; 45 | 46 | export const putItem = async ({ item }: { item: Item }) => { 47 | const command = new PutItemCommand({ 48 | TableName: TABLE_NAME, 49 | Item: marshall(item), 50 | }); 51 | 52 | return dynamoDb.send(command); 53 | }; 54 | 55 | export const getItem = async >(key: PrimaryKey) => { 56 | const command = new GetItemCommand({ 57 | TableName: TABLE_NAME, 58 | Key: marshall(key), 59 | }); 60 | 61 | const { Item = {} } = await dynamoDb.send(command); 62 | 63 | return unmarshall(Item) as I; 64 | }; 65 | 66 | export const updateItem = async >({ 67 | key, 68 | updateExpression, 69 | expressionAttributeValues, 70 | expressionAttributeNames, 71 | }: { 72 | key: PrimaryKey; 73 | updateExpression: string; 74 | expressionAttributeValues?: { [key: string]: any }; 75 | expressionAttributeNames?: { [key: string]: string }; 76 | }) => { 77 | const command = new UpdateItemCommand({ 78 | TableName: TABLE_NAME, 79 | Key: marshall(key), 80 | UpdateExpression: updateExpression, 81 | ReturnValues: 'ALL_NEW', 82 | ExpressionAttributeValues: marshall(expressionAttributeValues), 83 | ExpressionAttributeNames: expressionAttributeNames, 84 | }); 85 | 86 | const { Attributes = {} } = await dynamoDb.send(command); 87 | 88 | return unmarshall(Attributes) as I; 89 | }; 90 | 91 | export const query = async >({ 92 | expressionAttributeValues, 93 | keyConditionExpression, 94 | indexName, 95 | scanIndexForward, 96 | limit, 97 | }: { 98 | keyConditionExpression: string; 99 | expressionAttributeValues: { [key: string]: any }; 100 | indexName?: string; 101 | scanIndexForward?: boolean; 102 | limit?: number; 103 | }) => { 104 | const command = new QueryCommand({ 105 | TableName: TABLE_NAME, 106 | KeyConditionExpression: keyConditionExpression, 107 | ExpressionAttributeValues: marshall(expressionAttributeValues), 108 | IndexName: indexName, 109 | ScanIndexForward: scanIndexForward, 110 | Limit: limit, 111 | }); 112 | 113 | const { Items = [] } = await dynamoDb.send(command); 114 | 115 | return Items.map((item) => unmarshall(item) as I); 116 | }; 117 | -------------------------------------------------------------------------------- /src/api/slack.ts: -------------------------------------------------------------------------------- 1 | import { IncomingWebhook } from '@slack/webhook'; 2 | 3 | const url = process.env.SLACK_WEBHOOK_URL || ''; 4 | 5 | export const slack = new IncomingWebhook(url); 6 | -------------------------------------------------------------------------------- /src/binance-connector.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@binance/connector' { 2 | export type ExchangeInfo = { 3 | symbols: { 4 | symbol: string; 5 | filters: Array< 6 | | { 7 | filterType: 'PRICE_FILTER'; 8 | minPrice: string; 9 | maxPrice: string; 10 | tickSize: string; 11 | } 12 | | { 13 | filterType: 'PERCENT_PRICE'; 14 | multiplierUp: string; 15 | multiplierDown: string; 16 | avgPriceMins: number; 17 | } 18 | | { 19 | filterType: 'LOT_SIZE'; 20 | minQty: string; 21 | maxQty: string; 22 | stepSize: string; 23 | } 24 | | { 25 | filterType: 'MIN_NOTIONAL'; 26 | minNotional: string; 27 | applyToMarket: boolean; 28 | avgPriceMins: number; 29 | } 30 | | { filterType: 'ICEBERG_PARTS'; limit: number } 31 | | { 32 | filterType: 'MARKET_LOT_SIZE'; 33 | minQty: string; 34 | maxQty: string; 35 | stepSize: string; 36 | } 37 | | { filterType: 'MAX_NUM_ORDERS'; maxNumOrders: number } 38 | | { filterType: 'MAX_NUM_ALGO_ORDERS'; maxNumAlgoOrders: number } 39 | >; 40 | }[]; 41 | }; 42 | 43 | export type Order = { 44 | symbol: string; 45 | orderId: number; 46 | orderListId: number; 47 | clientOrderId: string; 48 | transactTime: number; 49 | price: string; 50 | origQty: string; 51 | executedQty: string; 52 | cummulativeQuoteQty: string; 53 | status: 'FILLED'; 54 | timeInForce: 'GTC'; 55 | type: 'MARKET'; 56 | side: 'SELL' | 'BUY'; 57 | fills: { 58 | price: string; 59 | qty: string; 60 | commission: string; 61 | commissionAsset: string; 62 | tradeId: number; 63 | }[]; 64 | }; 65 | 66 | export class Spot { 67 | constructor( 68 | apiKey: string, 69 | secretKey: string, 70 | options?: { baseURL?: string } 71 | ); 72 | 73 | public exchangeInfo(): Promise<{ data: ExchangeInfo }>; 74 | 75 | public account(): Promise<{ 76 | data: { balances: { asset: string; free: string }[] }; 77 | }>; 78 | 79 | public tickerPrice(asset: string): Promise<{ 80 | data: { 81 | symbol: string; 82 | price: string; 83 | }; 84 | }>; 85 | 86 | public newOrder( 87 | symbol: string, 88 | side: 'BUY' | 'SELL', 89 | type: 'MARKET', 90 | options?: Partial<{ quantity: number; quoteOrderQty: number }> 91 | ): Promise<{ data: Order }>; 92 | 93 | public savingsFlexibleRedeem( 94 | productId: string, 95 | amount: number, 96 | type: 'FAST' | 'NORMAL' 97 | ): Promise<{ data: {} }>; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export const isProduction = process.env.NODE_ENV === 'production'; 4 | 5 | const envPath = path.join( 6 | process.cwd(), 7 | `.env${isProduction ? '.production' : ''}` 8 | ); 9 | 10 | require('dotenv').config({ path: envPath }); 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './config'; 2 | 3 | import { startStrategy } from './strategy/strategy'; 4 | 5 | startStrategy(); 6 | -------------------------------------------------------------------------------- /src/strategy/falling.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arantespp/cryptobot/a6bf96643fa98ff73da984899cd4c325779c2fda/src/strategy/falling.ts -------------------------------------------------------------------------------- /src/strategy/first.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getExtremeProportions, 3 | getWalletProportionNormalized, 4 | doesHaveEnoughBalance, 5 | getEffectiveMinNotional, 6 | QUOTE_BASE_TICKER, 7 | executeQuoteOperation, 8 | canUseDepositsBalance, 9 | getEffectiveAssetAndQuotePropertiesFromBuyOrder, 10 | getMostProfitableAsset, 11 | formatAssetQuantity, 12 | calculateItemProfit, 13 | getAssetFromOrder, 14 | getAssetBuyOrdersWithLowestBuyPrice, 15 | willAssetBeTheLowestIfSold, 16 | } from './first'; 17 | 18 | import * as apiBinanceModule from '../api/binance'; 19 | import * as databaseModule from '../api/database'; 20 | 21 | jest.mock('../api/binance'); 22 | jest.mock('../api/database'); 23 | 24 | test('willAssetBeTheLowestIfSold', () => { 25 | const strategyData = { 26 | minNotional: 50, 27 | assets: { 28 | BTC: { 29 | totalValue: 100, 30 | }, 31 | ETH: { 32 | totalValue: 100, 33 | }, 34 | ADA: { 35 | totalValue: 100, 36 | }, 37 | VET: { 38 | totalValue: 100, 39 | }, 40 | }, 41 | } as any; 42 | 43 | const walletProportion = { 44 | BTC: 100, 45 | ETH: 101, 46 | ADA: 102, 47 | VET: 103, 48 | }; 49 | 50 | Object.keys(walletProportion).forEach((asset) => { 51 | expect( 52 | willAssetBeTheLowestIfSold({ 53 | strategyData, 54 | walletProportion, 55 | asset, 56 | }) 57 | ).toBe(true); 58 | }); 59 | }); 60 | 61 | test('getAssetBuyOrdersWithLowestBuyPrice', async () => { 62 | const buyOrderMock = jest 63 | .spyOn(databaseModule, 'query') 64 | .mockResolvedValueOnce([]); 65 | 66 | const asset = 'BTC'; 67 | await getAssetBuyOrdersWithLowestBuyPrice({ asset }); 68 | 69 | expect(buyOrderMock).toHaveBeenCalledWith( 70 | expect.objectContaining({ scanIndexForward: true, limit: 1 }) 71 | ); 72 | }); 73 | 74 | test.skip.each([ 75 | [ 76 | 'buy ETH', 77 | { 78 | BTC: 100, 79 | ETH: 100, 80 | ADA: 100, 81 | }, 82 | { 83 | minNotional: 10, 84 | assets: { 85 | [QUOTE_BASE_TICKER]: { totalValue: 1000 }, 86 | BTC: { 87 | totalValue: 100, 88 | }, 89 | ETH: { 90 | totalValue: 50, 91 | }, 92 | ADA: { 93 | totalValue: 100, 94 | }, 95 | }, 96 | }, 97 | 'ETH', 98 | ], 99 | [ 100 | 'buy ADA', 101 | { 102 | BTC: 100, 103 | ETH: 100, 104 | ADA: 100, 105 | }, 106 | { 107 | minNotional: 10, 108 | assets: { 109 | [QUOTE_BASE_TICKER]: { totalValue: 1000 }, 110 | BTC: { 111 | totalValue: 99, 112 | }, 113 | ETH: { 114 | totalValue: 90, 115 | }, 116 | ADA: { 117 | totalValue: 50, 118 | }, 119 | }, 120 | }, 121 | 'ADA', 122 | ], 123 | ])( 124 | 'executeQuoteOperation %#: %s', 125 | async (_, walletProportion, strategyData, assetToBuy) => { 126 | const buyOrderMock = jest.spyOn(apiBinanceModule, 'buyOrder'); 127 | 128 | jest 129 | .spyOn(databaseModule, 'getItem') 130 | .mockResolvedValue({ used: 1000, deposits: [{ amount: 1000 }] }); 131 | 132 | await executeQuoteOperation({ 133 | strategyData: strategyData as any, 134 | walletProportion, 135 | }); 136 | 137 | const quoteOrderQty = getEffectiveMinNotional({ 138 | strategyData: strategyData as any, 139 | }); 140 | 141 | expect(buyOrderMock).toHaveBeenCalledWith({ 142 | asset: assetToBuy, 143 | quoteOrderQty, 144 | }); 145 | } 146 | ); 147 | 148 | test.each(['ETH', 'ADA', 'BTC'])('getAssetFromOrder %s', (asset) => { 149 | const order = { symbol: `${asset}${QUOTE_BASE_TICKER}` } as any; 150 | expect(getAssetFromOrder({ order })).toBe(asset); 151 | }); 152 | 153 | test.each([ 154 | [ 155 | { pk: 'BTC', quotePrice: 100 }, 156 | { 157 | assets: { 158 | BTC: { 159 | price: 1000, 160 | }, 161 | }, 162 | }, 163 | 8.98001, 164 | ], 165 | [ 166 | { pk: 'BTC', quotePrice: 100 }, 167 | { 168 | assets: { 169 | BTC: { 170 | price: 100, 171 | }, 172 | }, 173 | }, 174 | -0.00199, 175 | ], 176 | ])('calculateItemProfit %#', (item, strategyData, result) => { 177 | expect(calculateItemProfit({ item, strategyData } as any)).toBeCloseTo( 178 | result, 179 | 4 180 | ); 181 | }); 182 | 183 | test.each([ 184 | [ 185 | 0.123456789, 186 | [ 187 | { 188 | filterType: 'LOT_SIZE', 189 | minQty: '0.00001000', 190 | maxQty: '9000.00000000', 191 | stepSize: '0.00001000', 192 | }, 193 | ], 194 | 0.12345, 195 | ], 196 | [ 197 | 1234.123456789, 198 | [ 199 | { 200 | filterType: 'LOT_SIZE', 201 | minQty: '0.00001000', 202 | maxQty: '9000.00000000', 203 | stepSize: '0.00001000', 204 | }, 205 | ], 206 | 1234.12345, 207 | ], 208 | [ 209 | 0.00000789, 210 | [ 211 | { 212 | filterType: 'LOT_SIZE', 213 | minQty: '0.00001000', 214 | maxQty: '9000.00000000', 215 | stepSize: '0.00001000', 216 | }, 217 | ], 218 | 0, 219 | ], 220 | ])('formatAssetQuantity %#', (assetQuantity, filters, result) => { 221 | expect(formatAssetQuantity({ assetQuantity, filters: filters as any })).toBe( 222 | result 223 | ); 224 | }); 225 | 226 | test.each([ 227 | [ 228 | { 229 | assets: { 230 | BTC: { 231 | price: 1010, 232 | }, 233 | ETH: { 234 | price: 110, 235 | }, 236 | ADA: { 237 | price: 20, 238 | }, 239 | }, 240 | }, 241 | [ 242 | { pk: 'BTC', quotePrice: 1000 }, 243 | { pk: 'ADA', quotePrice: 10 }, 244 | { pk: 'ETH', quotePrice: 100 }, 245 | ], 246 | { positionOfTheMostProfitableItem: 1 }, 247 | ], 248 | [ 249 | { 250 | assets: { 251 | BTC: { 252 | price: 1, 253 | }, 254 | ETH: { 255 | price: 110, 256 | }, 257 | ADA: { 258 | price: 20, 259 | }, 260 | }, 261 | }, 262 | [ 263 | { pk: 'BTC', quotePrice: 1000 }, 264 | { pk: 'ADA', quotePrice: 10 }, 265 | { pk: 'ETH', quotePrice: 100 }, 266 | ], 267 | { positionOfTheMostProfitableItem: 1 }, 268 | ], 269 | ])( 270 | 'getMostProfitableAsset', 271 | (strategyData, items, { positionOfTheMostProfitableItem }) => { 272 | expect(getMostProfitableAsset({ strategyData, items } as any)).toEqual( 273 | items[positionOfTheMostProfitableItem] 274 | ); 275 | } 276 | ); 277 | 278 | test.each([ 279 | [{ used: 1000, amount: 1000, quantityToBuy: 100, canUse: false }], 280 | [{ used: 900, amount: 1000, quantityToBuy: 10, canUse: true }], 281 | ])( 282 | 'canUseDepositsBalance %#', 283 | async ({ used, amount, quantityToBuy, canUse }) => { 284 | jest 285 | .spyOn(databaseModule, 'getItem') 286 | .mockResolvedValue({ used, deposits: [{ amount }] }); 287 | 288 | expect(await canUseDepositsBalance(quantityToBuy)).toBe(canUse); 289 | } 290 | ); 291 | 292 | test.each([ 293 | [ 294 | { fills: [{ price: 10000, qty: 100 }] }, 295 | { assetQuantity: 99.9, quotePrice: 10000 }, 296 | ], 297 | [ 298 | { 299 | fills: [ 300 | { price: 10000, qty: 100 }, 301 | { price: 10000, qty: 100 }, 302 | ], 303 | }, 304 | { assetQuantity: 199.8, quotePrice: 10000 }, 305 | ], 306 | [ 307 | { 308 | fills: [ 309 | { price: 10000, qty: 100 }, 310 | { price: 5000, qty: 100 }, 311 | ], 312 | }, 313 | { assetQuantity: 199.8, quotePrice: 7500 }, 314 | ], 315 | ])('getEffectiveAssetAndQuotePropertiesFromBuyOrder %#', (order, result) => { 316 | expect(getEffectiveAssetAndQuotePropertiesFromBuyOrder(order as any)).toEqual( 317 | result 318 | ); 319 | }); 320 | 321 | test('does not buy because does not have enough balance', async () => { 322 | const buyOrderMock = jest.spyOn(apiBinanceModule, 'buyOrder'); 323 | 324 | await executeQuoteOperation({ 325 | strategyData: { 326 | minNotional: 10, 327 | assets: { 328 | [QUOTE_BASE_TICKER]: { totalValue: 0 }, 329 | BTC: { 330 | totalValue: 300, 331 | }, 332 | ETH: { 333 | totalValue: 50, 334 | }, 335 | }, 336 | } as any, 337 | walletProportion: { 338 | BTC: 100, 339 | ETH: 50, 340 | }, 341 | }); 342 | 343 | expect(buyOrderMock).not.toHaveBeenCalled(); 344 | }); 345 | 346 | test.each([ 347 | [ 348 | { 349 | BTC: 100, 350 | ETH: 50, 351 | }, 352 | { 353 | BTC: 0.66666, 354 | ETH: 0.33333, 355 | }, 356 | ], 357 | [ 358 | { 359 | BTC: 100, 360 | ETH: 100, 361 | }, 362 | { 363 | BTC: 0.5, 364 | ETH: 0.5, 365 | }, 366 | ], 367 | [ 368 | { 369 | BTC: 1, 370 | ETH: 2, 371 | ADA: 3, 372 | VET: 4, 373 | }, 374 | { 375 | BTC: 0.1, 376 | ETH: 0.2, 377 | ADA: 0.3, 378 | VET: 0.4, 379 | }, 380 | ], 381 | ])('getWalletProportionNormalized %#', (wallet, normalized) => { 382 | const normalizedWallet = getWalletProportionNormalized(wallet); 383 | 384 | Object.keys(normalized).forEach((ticker) => { 385 | expect(normalizedWallet[ticker]).toBeCloseTo(normalized[ticker]); 386 | }); 387 | }); 388 | 389 | test.each([ 390 | [ 391 | { 392 | BTC: 100, 393 | ETH: 50, 394 | }, 395 | { 396 | assets: { 397 | BTC: { 398 | totalValue: 300, 399 | }, 400 | ETH: { 401 | totalValue: 50, 402 | }, 403 | }, 404 | }, 405 | { 406 | highest: 'BTC', 407 | lowest: 'ETH', 408 | sortedAssetsByRatioByAscending: ['ETH', 'BTC'], 409 | }, 410 | ], 411 | [ 412 | { 413 | BTC: 100, 414 | ETH: 50, 415 | ADA: 20, 416 | VET: 10, 417 | }, 418 | { 419 | assets: { 420 | BTC: { 421 | totalValue: 300, 422 | }, 423 | ETH: { 424 | totalValue: 50, 425 | }, 426 | ADA: { 427 | totalValue: 1, 428 | }, 429 | VET: { 430 | totalValue: 0, 431 | }, 432 | }, 433 | }, 434 | { 435 | highest: 'BTC', 436 | lowest: 'VET', 437 | sortedAssetsByRatioByAscending: ['VET', 'ADA', 'ETH', 'BTC'], 438 | }, 439 | ], 440 | [ 441 | { 442 | BTC: 100, 443 | ETH: 50, 444 | ADA: 20, 445 | VET: 10, 446 | }, 447 | { 448 | assets: { 449 | BTC: { 450 | totalValue: 100, 451 | }, 452 | ETH: { 453 | totalValue: 40, 454 | }, 455 | ADA: { 456 | totalValue: 15, 457 | }, 458 | VET: { 459 | totalValue: 5, 460 | }, 461 | }, 462 | }, 463 | { 464 | highest: 'BTC', 465 | lowest: 'VET', 466 | sortedAssetsByRatioByAscending: ['VET', 'ADA', 'ETH', 'BTC'], 467 | }, 468 | ], 469 | ])('getExtremeProportions %#', (walletProportion, strategyData, result) => { 470 | const extremeProportions = getExtremeProportions({ 471 | strategyData: strategyData as any, 472 | walletProportion, 473 | }); 474 | 475 | expect(extremeProportions).toMatchObject(result); 476 | }); 477 | 478 | test.each([ 479 | [ 480 | { minNotional: 10, assets: { [QUOTE_BASE_TICKER]: { totalValue: 10 } } }, 481 | false, 482 | ], 483 | [ 484 | { 485 | minNotional: 10, 486 | assets: { 487 | [QUOTE_BASE_TICKER]: { 488 | totalValue: getEffectiveMinNotional({ 489 | strategyData: { minNotional: 10 } as any, 490 | }), 491 | }, 492 | }, 493 | }, 494 | true, 495 | ], 496 | ])('doesHaveEnoughBalance %#', (strategyData, response) => { 497 | expect(doesHaveEnoughBalance({ strategyData: strategyData as any })).toBe( 498 | response 499 | ); 500 | }); 501 | -------------------------------------------------------------------------------- /src/strategy/first.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '@binance/connector'; 2 | import Debug from 'debug'; 3 | import { Decimal } from 'decimal.js'; 4 | import * as math from 'mathjs'; 5 | 6 | import { 7 | getStrategyData, 8 | StrategyData, 9 | buyOrder, 10 | sellOrder, 11 | QUOTE_BASE_TICKER, 12 | } from '../api/binance'; 13 | import * as database from '../api/database'; 14 | import { slack } from '../api/slack'; 15 | 16 | import { isProduction } from '../config'; 17 | 18 | import { WALLET } from './wallet'; 19 | 20 | import { 21 | MIN_NOTIONAL_MULTIPLIER, 22 | MIN_NOTIONAL_TO_TRADE, 23 | MIN_PROFIT, 24 | TRADE_FEE, 25 | LOWEST_QUANTITY_ASSETS_TO_NOT_TRADE, 26 | Z_SCORE_THRESHOLD_TO_SELL, 27 | } from './strategy.config'; 28 | 29 | export { QUOTE_BASE_TICKER }; 30 | 31 | type WalletProportion = { [asset: string]: number }; 32 | 33 | export const getWalletProportion = async (): Promise => { 34 | if (isProduction) { 35 | return WALLET; 36 | } 37 | 38 | /** 39 | * Test mode wallet. 40 | */ 41 | return { 42 | BTC: 100, 43 | ETH: 50, 44 | BNB: 20, 45 | LTC: 20, 46 | }; 47 | }; 48 | 49 | export const getWalletProportionTickers = (wallet: WalletProportion) => 50 | Object.keys(wallet); 51 | 52 | export const getWalletProportionNormalized = (wallet: WalletProportion) => { 53 | const walletTickers = Object.keys(wallet); 54 | 55 | const walletProportionSum = walletTickers.reduce( 56 | (sum, ticker) => sum + wallet[ticker], 57 | 0 58 | ); 59 | 60 | const normalized = { ...wallet }; 61 | 62 | walletTickers.forEach((ticker) => { 63 | normalized[ticker] /= walletProportionSum; 64 | }); 65 | 66 | return normalized; 67 | }; 68 | 69 | /** 70 | * 71 | * @param strategyData: StrategyData 72 | * @returns object.highest: ticker of the highest proportion in the wallet. 73 | * object.lowest: ticker of the lowest proportion in the wallet. 74 | */ 75 | export const getExtremeProportions = ({ 76 | strategyData, 77 | walletProportion, 78 | }: { 79 | strategyData: StrategyData; 80 | walletProportion: WalletProportion; 81 | }) => { 82 | const debug = Debug('CryptoBot:getExtremeProportions'); 83 | 84 | const tickers = getWalletProportionTickers(walletProportion); 85 | 86 | const { assets } = strategyData; 87 | 88 | const walletTotalValue = tickers.reduce( 89 | (acc, ticker) => acc + (assets[ticker]?.totalValue || 0), 90 | 0 91 | ); 92 | 93 | const currentWalletProportion = tickers.reduce((acc, ticker) => { 94 | acc[ticker] = (assets[ticker]?.totalValue || 0) / (walletTotalValue || 1); 95 | return acc; 96 | }, {}); 97 | 98 | const targetWalletProportionNormalized = 99 | getWalletProportionNormalized(walletProportion); 100 | 101 | const ratio = tickers.reduce((acc, ticker) => { 102 | acc[ticker] = 103 | (currentWalletProportion[ticker] - 104 | targetWalletProportionNormalized[ticker]) / 105 | targetWalletProportionNormalized[ticker]; 106 | return acc; 107 | }, {} as { [ticker: string]: number }); 108 | 109 | const findTickerByRatio = (ratioValue: number) => 110 | tickers.find((ticker) => ratio[ticker] === ratioValue) as string; 111 | 112 | const sortedAssetsByRatioByAscending = Object.values(ratio) 113 | .sort((a, b) => a - b) 114 | .map((ratioValue) => findTickerByRatio(ratioValue)); 115 | 116 | const minRatio = Math.min(...Object.values(ratio)); 117 | 118 | const maxRatio = Math.max(...Object.values(ratio)); 119 | 120 | const lowest = findTickerByRatio(minRatio); 121 | 122 | const highest = findTickerByRatio(maxRatio); 123 | 124 | const ratioMean = math.mean(Object.values(ratio)); 125 | 126 | const ratioStd = math.std(Object.values(ratio)); 127 | 128 | const zScore = tickers.reduce((acc, ticker) => { 129 | acc[ticker] = (ratio[ticker] - ratioMean) / ratioStd; 130 | return acc; 131 | }, {} as { [ticker: string]: number }); 132 | 133 | const zScoreAmplitude = 134 | Math.max(...Object.values(zScore)) - Math.min(...Object.values(zScore)); 135 | 136 | return { 137 | highest, 138 | lowest, 139 | ratio, 140 | currentWalletProportion, 141 | sortedAssetsByRatioByAscending, 142 | zScore, 143 | zScoreAmplitude, 144 | }; 145 | }; 146 | 147 | /** 148 | * Multiplied by `MIN_NOTIONAL_MULTIPLIER` to have a margin for filters. 149 | */ 150 | export const getEffectiveMinNotional = ({ 151 | strategyData, 152 | }: { 153 | strategyData: StrategyData; 154 | }) => strategyData.minNotional * MIN_NOTIONAL_MULTIPLIER; 155 | 156 | export const doesHaveEnoughBalance = ({ 157 | strategyData, 158 | }: { 159 | strategyData: StrategyData; 160 | }) => { 161 | return ( 162 | strategyData.assets[QUOTE_BASE_TICKER].totalValue >= 163 | getEffectiveMinNotional({ strategyData }) 164 | ); 165 | }; 166 | 167 | const DEPOSITS_KEY = { 168 | pk: QUOTE_BASE_TICKER, 169 | sk: 'DEPOSITS', 170 | }; 171 | 172 | type Deposits = { 173 | deposits: { amount: number }[]; 174 | used: number; 175 | }; 176 | 177 | export const canUseDepositsBalance = async (amount: number) => { 178 | const debug = Debug('CryptoBot:canUseDepositsBalance'); 179 | 180 | const { used, deposits } = await database.getItem(DEPOSITS_KEY); 181 | 182 | const sumDeposits = deposits.reduce((acc, { amount }) => acc + amount, 0); 183 | 184 | const remaining = sumDeposits - used; 185 | 186 | const canUse = remaining >= amount; 187 | 188 | debug({ sumDeposits, used, remaining, canUse }); 189 | 190 | if (remaining < amount) { 191 | return false; 192 | } 193 | 194 | return true; 195 | }; 196 | 197 | export const updateUsedDepositsBalance = async (amount: number) => { 198 | const debug = Debug('CryptoBot:updateUsedDepositsBalance'); 199 | 200 | const { used } = await database.updateItem({ 201 | key: DEPOSITS_KEY, 202 | updateExpression: 'ADD used :amount', 203 | expressionAttributeValues: { ':amount': amount }, 204 | }); 205 | 206 | debug({ newUsed: used, amount }); 207 | }; 208 | 209 | export const getEffectiveAssetAndQuotePropertiesFromBuyOrder = ( 210 | order: Order 211 | ) => { 212 | const debug = Debug('CryptoBot:getAssetAndQuotePropertiesFromBuyOrder'); 213 | 214 | const fills = order.fills.map((fill) => ({ 215 | ...fill, 216 | effectiveQuantity: Number(fill.qty) * (1 - TRADE_FEE), 217 | })); 218 | 219 | /** 220 | * It'll be the same amount that will be sold in the sell order. 221 | */ 222 | const assetQuantity = fills.reduce( 223 | (acc, { effectiveQuantity }) => acc + effectiveQuantity, 224 | 0 225 | ); 226 | 227 | /** 228 | * It'll be used to determine if this croton is profitable. 229 | */ 230 | const quotePrice = 231 | fills.reduce( 232 | (acc, { effectiveQuantity, price }) => 233 | acc + effectiveQuantity * Number(price), 234 | 0 235 | ) / assetQuantity; 236 | 237 | debug({ assetQuantity, quotePrice, order }); 238 | 239 | return { assetQuantity, quotePrice }; 240 | }; 241 | 242 | type BaseOrderOnDatabase = { 243 | pk: string; 244 | sk: string; 245 | order: Order; 246 | assetQuantity: number; 247 | quotePrice: number; 248 | }; 249 | 250 | type BuyOrderOnDatabase = BaseOrderOnDatabase & { 251 | status?: string; 252 | usedDepositsBalance: boolean; 253 | }; 254 | 255 | export const saveBuyOrder = async ({ 256 | order, 257 | usedDepositsBalance, 258 | }: { 259 | order: Order; 260 | usedDepositsBalance: boolean; 261 | }) => { 262 | const debug = Debug('CryptoBot:saveBuyOrder'); 263 | 264 | debug('Saving buy order'); 265 | 266 | const date = new Date().toISOString(); 267 | 268 | const { assetQuantity, quotePrice } = 269 | getEffectiveAssetAndQuotePropertiesFromBuyOrder(order); 270 | 271 | const asset = getAssetFromOrder({ order }); 272 | 273 | const status = `BUY_PRICE_${quotePrice}`; 274 | 275 | const item: BuyOrderOnDatabase = { 276 | pk: asset, 277 | sk: `ORDER_BUY_${date}`, 278 | status, 279 | order, 280 | usedDepositsBalance, 281 | assetQuantity, 282 | quotePrice, 283 | }; 284 | 285 | debug('Saving item'); 286 | 287 | await database.putItem({ item }); 288 | 289 | debug(item); 290 | 291 | return item; 292 | }; 293 | 294 | export const getAssetFromOrder = ({ order }: { order: Order }) => { 295 | return order.symbol.replace(QUOTE_BASE_TICKER, ''); 296 | }; 297 | 298 | const updateWalletAssetsEarned = async ({ order }: { order: Order }) => { 299 | const debug = Debug('CryptoBot:updateWalletAssetsEarned'); 300 | 301 | const { assetQuantity } = 302 | getEffectiveAssetAndQuotePropertiesFromBuyOrder(order); 303 | 304 | const asset = getAssetFromOrder({ order }); 305 | 306 | const { side } = order; 307 | 308 | const quantity = side === 'BUY' ? assetQuantity : -assetQuantity; 309 | 310 | const data = await database.updateItem({ 311 | key: { pk: 'WALLET', sk: 'CURRENT_EARNINGS' }, 312 | updateExpression: 'ADD #asset :quantity', 313 | expressionAttributeValues: { 314 | ':quantity': quantity, 315 | }, 316 | expressionAttributeNames: { 317 | '#asset': asset, 318 | }, 319 | }); 320 | 321 | debug({ side, quantity }); 322 | 323 | debug(`Update ${asset} earnings: ${data[asset]}`); 324 | }; 325 | 326 | const wasOrderSuccessful = ({ order }: { order: Order }) => { 327 | return order.status === 'FILLED'; 328 | }; 329 | 330 | /** 331 | * Quote operation is the same as buy assets using the quote currency. 332 | */ 333 | export const executeQuoteOperation = async ({ 334 | strategyData, 335 | walletProportion, 336 | asset, 337 | }: { 338 | strategyData: StrategyData; 339 | walletProportion: WalletProportion; 340 | asset?: string; 341 | }): Promise => { 342 | const debug = Debug('CryptoBot:executeQuoteOperation'); 343 | 344 | debug('Starting Quote Operation'); 345 | 346 | if (!doesHaveEnoughBalance({ strategyData })) { 347 | debug('Not enough balance'); 348 | return false; 349 | } 350 | 351 | const { lowest, highest } = getExtremeProportions({ 352 | strategyData, 353 | walletProportion, 354 | }); 355 | 356 | const quoteOrderQty = getEffectiveMinNotional({ strategyData }); 357 | 358 | const canUseDepositsBalanceBool = await canUseDepositsBalance(quoteOrderQty); 359 | 360 | debug({ lowest, highest, quoteOrderQty, canUseDepositsBalanceBool }); 361 | 362 | /** 363 | * When `asset` is defined, it's because it's a second buy. 364 | */ 365 | const assetToBuy = asset || lowest; 366 | 367 | const order = await buyOrder({ quoteOrderQty, asset: assetToBuy }); 368 | 369 | debug(order); 370 | 371 | if (!wasOrderSuccessful({ order })) { 372 | debug('Buy order was not successful'); 373 | return false; 374 | } 375 | 376 | const buyItem = await saveBuyOrder({ 377 | order, 378 | usedDepositsBalance: canUseDepositsBalanceBool, 379 | }); 380 | 381 | await slack.send( 382 | `${buyItem.assetQuantity} of ${assetToBuy} was brought by $${ 383 | buyItem.quotePrice 384 | }${canUseDepositsBalanceBool ? ' (deposit)' : ''}` 385 | ); 386 | 387 | if (canUseDepositsBalanceBool) { 388 | debug('Updating used deposits balance'); 389 | await updateUsedDepositsBalance(Number(order.cummulativeQuoteQty)); 390 | } else { 391 | debug('Cannot update used deposits balance. Updating assets earnings'); 392 | await updateWalletAssetsEarned({ order }); 393 | } 394 | 395 | debug('Quote Operation Finished'); 396 | 397 | return true; 398 | }; 399 | 400 | const getTickersFromStrategyData = ({ 401 | strategyData, 402 | }: { 403 | strategyData: StrategyData; 404 | }) => Object.keys(strategyData.assets); 405 | 406 | export const getAssetBuyOrdersWithLowestBuyPrice = async ({ 407 | asset, 408 | }: { 409 | asset: string; 410 | }) => { 411 | const items = await database.query({ 412 | keyConditionExpression: 'pk = :pk', 413 | expressionAttributeValues: { ':pk': asset }, 414 | indexName: 'pk-status-index', 415 | scanIndexForward: true, 416 | limit: 1, 417 | }); 418 | 419 | const itemWithLowestBuyPrice = items[0]; 420 | 421 | return itemWithLowestBuyPrice; 422 | }; 423 | 424 | const getAssetsBuyOrdersWithLowestBuyPrice = async ({ 425 | strategyData, 426 | }: { 427 | strategyData: StrategyData; 428 | }) => { 429 | const assets = getTickersFromStrategyData({ strategyData }); 430 | 431 | const lowestBuyPrices = await Promise.all( 432 | assets.map((asset) => getAssetBuyOrdersWithLowestBuyPrice({ asset })) 433 | ); 434 | 435 | const items = lowestBuyPrices.filter((item) => !!item); 436 | 437 | return items; 438 | }; 439 | 440 | const getAssetCurrentPrice = ({ 441 | asset, 442 | strategyData, 443 | }: { 444 | asset: string; 445 | strategyData: StrategyData; 446 | }) => { 447 | return strategyData.assets[asset].price; 448 | }; 449 | 450 | export const calculateItemProfit = ({ 451 | item, 452 | strategyData, 453 | }: { 454 | item: BuyOrderOnDatabase; 455 | strategyData: StrategyData; 456 | }) => { 457 | const { quotePrice } = item; 458 | const currentPrice = getAssetCurrentPrice({ asset: item.pk, strategyData }); 459 | return (currentPrice / quotePrice) * (1 - TRADE_FEE) ** 2 - 1; 460 | }; 461 | 462 | export const getMostProfitableAsset = ({ 463 | strategyData, 464 | items, 465 | }: { 466 | strategyData: StrategyData; 467 | items: BuyOrderOnDatabase[]; 468 | }) => { 469 | const mostProfitableAsset = items.reduce((acc, cur) => { 470 | return calculateItemProfit({ strategyData, item: cur }) > 471 | calculateItemProfit({ strategyData, item: acc }) 472 | ? cur 473 | : acc; 474 | }, items[0]); 475 | 476 | return mostProfitableAsset; 477 | }; 478 | 479 | export const formatAssetQuantity = ({ 480 | assetQuantity, 481 | filters = [], 482 | }: { 483 | filters: StrategyData['assets'][string]['filters']; 484 | assetQuantity: number; 485 | }): number => { 486 | let value = new Decimal(assetQuantity); 487 | 488 | const lotSizeFilter = filters.find( 489 | (filter) => filter?.filterType === 'LOT_SIZE' 490 | ); 491 | 492 | if (lotSizeFilter && lotSizeFilter.filterType === 'LOT_SIZE') { 493 | const stepSize = new Decimal(Number(lotSizeFilter.stepSize)); 494 | value = value.toDecimalPlaces(stepSize.decimalPlaces(), Decimal.ROUND_DOWN); 495 | 496 | if (value.lessThan(Number(lotSizeFilter.minQty))) { 497 | return 0; 498 | } 499 | } 500 | 501 | return value.toNumber(); 502 | }; 503 | 504 | type SellOrderOnDatabase = BaseOrderOnDatabase & { buyOrder: string }; 505 | 506 | export const saveSellOrder = async ({ 507 | order, 508 | buyItem, 509 | }: { 510 | order: Order; 511 | buyItem: BuyOrderOnDatabase; 512 | }) => { 513 | const debug = Debug('CryptoBot:saveSellOrder'); 514 | 515 | debug('Saving sell order'); 516 | 517 | const date = new Date().toISOString(); 518 | 519 | const { assetQuantity, quotePrice } = 520 | getEffectiveAssetAndQuotePropertiesFromBuyOrder(order); 521 | 522 | const asset = getAssetFromOrder({ order }); 523 | 524 | const item: SellOrderOnDatabase = { 525 | pk: asset, 526 | sk: `ORDER_SELL_${date}`, 527 | order, 528 | assetQuantity, 529 | quotePrice, 530 | buyOrder: [buyItem.pk, buyItem.sk].join('##'), 531 | }; 532 | 533 | await database.putItem({ item }); 534 | 535 | debug(item); 536 | 537 | return item; 538 | }; 539 | 540 | export const updateBuyOrderStatus = async ({ 541 | buyItem, 542 | sellItem, 543 | }: { 544 | buyItem: BuyOrderOnDatabase; 545 | sellItem: SellOrderOnDatabase; 546 | }) => { 547 | const debug = Debug('CryptoBot:updateBuyOrderStatus'); 548 | 549 | debug('Updating buy order status'); 550 | 551 | /** 552 | * By removing status, we can't query for the order thus it won't be sold. 553 | */ 554 | await database.updateItem({ 555 | key: { pk: buyItem.pk, sk: buyItem.sk }, 556 | updateExpression: 'REMOVE #status SET sellOrder = :sellOrder', 557 | expressionAttributeValues: { 558 | ':sellOrder': [sellItem.pk, sellItem.sk].join('##'), 559 | }, 560 | expressionAttributeNames: { '#status': 'status' }, 561 | }); 562 | 563 | debug(`Updated buy order status: ${buyItem.pk}${buyItem.sk}`); 564 | }; 565 | 566 | const sellBoughtAsset = async ({ 567 | item, 568 | strategyData, 569 | }: { 570 | item: BuyOrderOnDatabase; 571 | strategyData: StrategyData; 572 | }) => { 573 | const debug = Debug('CryptoBot:sellBoughtAsset'); 574 | 575 | const asset = item.pk; 576 | 577 | debug(`Selling bought asset ${asset}`); 578 | 579 | const quantity = formatAssetQuantity({ 580 | assetQuantity: item.assetQuantity, 581 | filters: strategyData.assets[asset].filters, 582 | }); 583 | 584 | const order = await sellOrder({ asset, quantity }); 585 | 586 | debug(order); 587 | 588 | return order; 589 | }; 590 | 591 | /** 592 | * Check if the asset the asset will become the lowest ratio if sold and the 593 | * same value buy the current lowest ratio. 594 | */ 595 | export const willAssetBeTheLowestIfSold = ({ 596 | asset, 597 | strategyData, 598 | walletProportion, 599 | }: { 600 | asset: string; 601 | strategyData: StrategyData; 602 | walletProportion: WalletProportion; 603 | }) => { 604 | const { lowest: currentLowest } = getExtremeProportions({ 605 | walletProportion, 606 | strategyData, 607 | }); 608 | 609 | const newStrategyData: typeof strategyData = JSON.parse( 610 | JSON.stringify(strategyData) 611 | ); 612 | 613 | const totalToTrade = 3 * strategyData.minNotional; 614 | 615 | newStrategyData.assets[asset].totalValue -= totalToTrade; 616 | newStrategyData.assets[currentLowest].totalValue += totalToTrade; 617 | 618 | const newExtremeValues = getExtremeProportions({ 619 | strategyData: newStrategyData, 620 | walletProportion, 621 | }); 622 | 623 | return newExtremeValues.lowest === asset; 624 | }; 625 | 626 | const getLowestBuyPricesFiltered = ({ 627 | strategyData, 628 | walletProportion, 629 | lowestBuyPrices, 630 | }: { 631 | strategyData: StrategyData; 632 | walletProportion: WalletProportion; 633 | lowestBuyPrices: BuyOrderOnDatabase[]; 634 | }) => { 635 | const { sortedAssetsByRatioByAscending, zScore } = getExtremeProportions({ 636 | strategyData, 637 | walletProportion, 638 | }); 639 | 640 | const lowestRatioAssets = sortedAssetsByRatioByAscending.slice( 641 | 0, 642 | LOWEST_QUANTITY_ASSETS_TO_NOT_TRADE 643 | ); 644 | 645 | const filtered = lowestBuyPrices 646 | .map((item) => ({ 647 | ...item, 648 | profit: calculateItemProfit({ item, strategyData }), 649 | zScore: zScore[item.pk], 650 | })) 651 | /** 652 | * Remove the assets with the lowest ratio. 653 | */ 654 | .filter((item) => !lowestRatioAssets.includes(item.pk)) 655 | /** 656 | * Only return the items that have positive profit or if it has a huge 657 | * z-score. 658 | */ 659 | .filter( 660 | (item) => 661 | item.profit > MIN_PROFIT || item.zScore > Z_SCORE_THRESHOLD_TO_SELL 662 | ) 663 | /** 664 | * Don't sell assets that have totalValue less than 665 | * MIN_NOTIONAL_TO_TRADE effective minNotional. 666 | */ 667 | .filter( 668 | (item) => 669 | strategyData.assets[item.pk].totalValue > 670 | MIN_NOTIONAL_TO_TRADE * getEffectiveMinNotional({ strategyData }) 671 | ) 672 | /** 673 | * Don't sell if the asset will become the lowest if sold. 674 | */ 675 | .filter( 676 | (item) => 677 | !willAssetBeTheLowestIfSold({ 678 | asset: item.pk, 679 | strategyData, 680 | walletProportion, 681 | }) 682 | ) 683 | /** 684 | * From the most profitable to the least profitable. 685 | */ 686 | .sort((a, b) => b.profit - a.profit); 687 | 688 | return filtered; 689 | }; 690 | 691 | export const executeAssetsOperation = async ({ 692 | strategyData, 693 | walletProportion, 694 | }: { 695 | strategyData: StrategyData; 696 | walletProportion: WalletProportion; 697 | }): Promise => { 698 | const debug = Debug('CryptoBot:executeAssetsOperation'); 699 | 700 | debug('Starting Assets Operation'); 701 | 702 | const allLowestBuyPrices = await getAssetsBuyOrdersWithLowestBuyPrice({ 703 | strategyData, 704 | }); 705 | 706 | const lowestBuyPrices = getLowestBuyPricesFiltered({ 707 | lowestBuyPrices: allLowestBuyPrices, 708 | strategyData, 709 | walletProportion, 710 | }); 711 | 712 | const itemToSell = lowestBuyPrices[0]; 713 | 714 | if (!itemToSell) { 715 | debug('There are no items to sell.'); 716 | return false; 717 | } 718 | 719 | debug({ itemToSell }); 720 | 721 | try { 722 | const order = await sellBoughtAsset({ 723 | strategyData, 724 | item: itemToSell, 725 | }); 726 | 727 | if (!wasOrderSuccessful({ order })) { 728 | debug(`Sell order was not successful`); 729 | return false; 730 | } 731 | 732 | const sellItem = await saveSellOrder({ 733 | order, 734 | buyItem: itemToSell, 735 | }); 736 | 737 | /** 738 | * `sellItem`: item that was sold. 739 | * `mostProfitableAsset`: that was bought before and was sold as `sellItem`. 740 | */ 741 | const profit = 742 | (sellItem.quotePrice - itemToSell.quotePrice) * sellItem.assetQuantity; 743 | 744 | await Promise.all([ 745 | slack.send( 746 | `${sellItem.assetQuantity} of ${sellItem.pk} was *SOLD* by $${sellItem.quotePrice}. It was bought by $${itemToSell.quotePrice} (profit of $${profit}).` 747 | ), 748 | updateBuyOrderStatus({ buyItem: itemToSell, sellItem }), 749 | updateWalletAssetsEarned({ order }), 750 | ]); 751 | 752 | return sellItem; 753 | } catch (error) { 754 | debug('Cannot perform selling order. Error:'); 755 | console.error(error); 756 | return false; 757 | } 758 | }; 759 | 760 | export const runFirstStrategy = async () => { 761 | const debug = Debug('CryptoBot:runFirstStrategy'); 762 | 763 | debug('Run First Strategy'); 764 | 765 | const walletProportion = await getWalletProportion(); 766 | 767 | const tickers = getWalletProportionTickers(walletProportion); 768 | 769 | const strategyData = await getStrategyData(tickers); 770 | 771 | debug({ strategyData, walletProportion }); 772 | 773 | debug('Executing quote operation for the FIRST time'); 774 | 775 | const wasQuoteOperationExecuted = await executeQuoteOperation({ 776 | strategyData, 777 | walletProportion, 778 | }); 779 | 780 | if (!wasQuoteOperationExecuted) { 781 | debug('Quote Operation was not executed in the FIRST time'); 782 | debug('Executing Assets Operation'); 783 | 784 | const wasAssetsOperationExecuted = await executeAssetsOperation({ 785 | strategyData, 786 | walletProportion, 787 | }); 788 | 789 | if (wasAssetsOperationExecuted) { 790 | debug('Executing quote operation for the SECOND time'); 791 | 792 | const { lowest: lowestAssetBeforeSecondQuoteOperation } = 793 | getExtremeProportions({ 794 | strategyData, 795 | walletProportion, 796 | }); 797 | 798 | /** 799 | * Get updated data. 800 | */ 801 | const newStrategyData = await getStrategyData(tickers); 802 | 803 | await executeQuoteOperation({ 804 | strategyData: newStrategyData, 805 | walletProportion, 806 | asset: lowestAssetBeforeSecondQuoteOperation, 807 | }); 808 | } 809 | } 810 | 811 | debug('First Strategy Finished'); 812 | }; 813 | 814 | export const slackLogs = async () => { 815 | const walletProportion = await getWalletProportion(); 816 | 817 | const tickers = getWalletProportionTickers(walletProportion); 818 | 819 | const strategyData = await getStrategyData(tickers); 820 | 821 | const { zScore } = getExtremeProportions({ 822 | strategyData, 823 | walletProportion, 824 | }); 825 | 826 | const total = Object.values(strategyData.assets).reduce( 827 | (acc, asset) => acc + asset.totalValue, 828 | 0 829 | ); 830 | 831 | let text = tickers 832 | .sort( 833 | (a, b) => 834 | strategyData.assets[b].totalValue - strategyData.assets[a].totalValue 835 | ) 836 | .map((ticker) => { 837 | const r = Math.round(zScore[ticker] * 100) / 100; 838 | const v = strategyData.assets[ticker].totalValue.toFixed(2); 839 | const q = strategyData.assets[ticker].quantity.toPrecision(6); 840 | return `• ${ticker} (*${r}*): $${v} --- Qty: ${q}`; 841 | }) 842 | .join('\n'); 843 | 844 | text += `\n\nTotal: $${total.toFixed(2)}`; 845 | 846 | await slack.send({ 847 | blocks: [ 848 | { 849 | type: 'section', 850 | text: { 851 | type: 'mrkdwn', 852 | text, 853 | }, 854 | }, 855 | ], 856 | }); 857 | }; 858 | -------------------------------------------------------------------------------- /src/strategy/strategy.config.ts: -------------------------------------------------------------------------------- 1 | import { WALLET } from './wallet'; 2 | 3 | export const TRADE_FEE = 0.001; 4 | 5 | export const MIN_PROFIT = 2.5 / 100; 6 | 7 | /** 8 | * Used to determining the quantity of the asset to buy. 9 | */ 10 | export const MIN_NOTIONAL_MULTIPLIER = 1.5; 11 | 12 | /** 13 | * The total value that a asset must have to be traded. 14 | */ 15 | export const MIN_NOTIONAL_TO_TRADE = 10; 16 | 17 | /** 18 | * Trade only a percentage of the wallet. 19 | */ 20 | export const LOWEST_QUANTITY_ASSETS_TO_NOT_TRADE = Math.round( 21 | Object.keys(WALLET).length * 0.5 22 | ); 23 | 24 | /** 25 | * Sell the asset if its z-score is greater than this value. 26 | */ 27 | export const Z_SCORE_THRESHOLD_TO_SELL = 2; 28 | -------------------------------------------------------------------------------- /src/strategy/strategy.ts: -------------------------------------------------------------------------------- 1 | import cron from 'node-cron'; 2 | 3 | import { slack } from '../api/slack'; 4 | 5 | import { isProduction } from '../config'; 6 | 7 | import { runFirstStrategy, slackLogs } from './first'; 8 | import { updateEarningsSnapshot } from './updateEarningsSnapshot'; 9 | 10 | const errorHandlerWrapper = (fn: () => any) => async () => { 11 | try { 12 | await fn(); 13 | } catch (error: any) { 14 | if (error?.response?.data) { 15 | console.error(error.response.data); 16 | } else { 17 | console.error(error); 18 | } 19 | } 20 | }; 21 | 22 | const schedule = (cronExpression: string, fn: () => void) => { 23 | cron.schedule(cronExpression, errorHandlerWrapper(fn)); 24 | }; 25 | 26 | export const startStrategy = () => { 27 | const message = 28 | 'Starting Strategy' + (isProduction ? ' in production mode' : ''); 29 | 30 | console.log(message); 31 | 32 | slack.send(message); 33 | 34 | schedule('*/30 * * * * *', runFirstStrategy); 35 | 36 | schedule('0 * * * *', slackLogs); 37 | slackLogs(); 38 | 39 | schedule('15 59 * * * *', updateEarningsSnapshot); 40 | }; 41 | -------------------------------------------------------------------------------- /src/strategy/updateEarningsSnapshot.ts: -------------------------------------------------------------------------------- 1 | import * as dateFns from 'date-fns'; 2 | import Debug from 'debug'; 3 | import Decimal from 'decimal.js'; 4 | 5 | import { getStrategyData } from '../api/binance'; 6 | 7 | import * as database from '../api/database'; 8 | 9 | import * as strategyConfig from './strategy.config'; 10 | 11 | type Snapshot = { 12 | [asset: string]: { earnings: number; value: number }; 13 | }; 14 | 15 | const pk = 'WALLET'; 16 | 17 | const getSk = (date: string) => `EARNINGS_SNAPSHOT_${date}`; 18 | 19 | const formatNumber = (value: number): number => 20 | new Decimal(value).toSignificantDigits(7).toNumber(); 21 | 22 | const getValueFromSnapshot = (snapshot: Snapshot): number => { 23 | let value = 0; 24 | 25 | Object.keys(snapshot).forEach((asset) => { 26 | value += snapshot[asset].value; 27 | }); 28 | 29 | return formatNumber(value); 30 | }; 31 | 32 | export const updateEarningsSnapshot = async () => { 33 | const debug = Debug('CryptoBot:updateEarningsSnapshot'); 34 | 35 | const today = dateFns.format(new Date(), 'yyyy-MM-dd'); 36 | 37 | const yesterday = dateFns.format( 38 | dateFns.addDays(new Date(), -1), 39 | 'yyyy-MM-dd' 40 | ); 41 | 42 | const { 43 | pk: _pk, 44 | sk: _sk, 45 | ...assetsEarning 46 | } = await database.getItem({ 47 | pk, 48 | sk: 'CURRENT_EARNINGS', 49 | }); 50 | 51 | const tickers = Object.keys(assetsEarning); 52 | 53 | const strategyData = await getStrategyData(tickers); 54 | 55 | const { snapshot: yesterdaySnapshot = {} } = await database.getItem<{ 56 | snapshot: Snapshot; 57 | }>({ 58 | pk, 59 | sk: getSk(yesterday), 60 | }); 61 | 62 | const snapshot: Snapshot = tickers.reduce((acc, asset) => { 63 | const earnings = assetsEarning[asset]; 64 | const price = strategyData.assets[asset].price; 65 | 66 | acc[asset] = { 67 | earnings: formatNumber(earnings), 68 | value: formatNumber(earnings * price), 69 | }; 70 | 71 | return acc; 72 | }, {}); 73 | 74 | const diff: Snapshot = tickers.reduce((acc, asset) => { 75 | const earnings = formatNumber( 76 | snapshot[asset].earnings - (yesterdaySnapshot[asset]?.earnings || 0) 77 | ); 78 | const price = strategyData.assets[asset].price; 79 | 80 | acc[asset] = { 81 | earnings, 82 | value: formatNumber(earnings * price), 83 | }; 84 | 85 | return acc; 86 | }, {}); 87 | 88 | const snapshotValue = getValueFromSnapshot(snapshot); 89 | 90 | const diffValue = getValueFromSnapshot(diff); 91 | 92 | debug({ 93 | strategyConfig, 94 | tickers, 95 | snapshot, 96 | diff, 97 | snapshotValue, 98 | diffValue, 99 | yesterdaySnapshot, 100 | }); 101 | 102 | await database.putItem({ 103 | item: { 104 | pk, 105 | sk: getSk(today), 106 | snapshot, 107 | diff, 108 | strategyConfig, 109 | snapshotValue, 110 | diffValue, 111 | }, 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /src/strategy/wallet.ts: -------------------------------------------------------------------------------- 1 | const BETS = [ 2 | 'BTT', 3 | 'STMX', 4 | 'ONE', 5 | 'FOR', 6 | 'TRX', 7 | 'SUN', 8 | 'FIL', 9 | 'HOT', 10 | 'SC', 11 | 'ALICE', 12 | // 'AKRO', // Do not exist in the BUSD market. 13 | 'REEF', 14 | 'PSG', 15 | 'SLP', 16 | // 'SANTOS', // Do not exist in the BUSD market. 17 | 'ATOM', 18 | 'FXS', 19 | 'SCRT', 20 | 'CELR', 21 | ]; 22 | 23 | const BETS_WEIGHT = 1; 24 | 25 | /** 26 | * Proportional to market cap. 27 | * https://coinmarketcap.com/all/views/all/ 28 | */ 29 | export const WALLET = { 30 | // ...BETS.reduce((acc, bet) => { 31 | // acc[bet] = BETS_WEIGHT; 32 | // return acc; 33 | // }, {}), 34 | BTC: 30, 35 | ETH: 30, 36 | BNB: 20, 37 | SOL: 20, 38 | ADA: 20, 39 | XRP: 20, 40 | DOT: 15, 41 | DOGE: 15, 42 | AVAX: 15, 43 | LUNA: 15, 44 | SHIB: 15, 45 | LTC: 10, 46 | UNI: 10, 47 | LINK: 10, 48 | MATIC: 10, 49 | VET: 10, 50 | MANA: 10, 51 | GRT: 10, 52 | XLM: 5, 53 | FTM: 5, 54 | AXS: 5, 55 | SAND: 5, 56 | GALA: 5, 57 | XMR: 5, 58 | STX: 5, 59 | CHZ: 5, 60 | }; 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "files": true // https://stackoverflow.com/a/68488480/8786986 4 | }, 5 | "compilerOptions": { 6 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 7 | 8 | /* Projects */ 9 | // "incremental": true, /* Enable incremental compilation */ 10 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 11 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 12 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 13 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 14 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 15 | 16 | /* Language and Environment */ 17 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 18 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 25 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | 29 | /* Modules */ 30 | "module": "commonjs" /* Specify what module code is generated. */, 31 | // "rootDir": "./", /* Specify the root folder within your source files. */ 32 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 34 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 37 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files */ 40 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 41 | 42 | /* JavaScript Support */ 43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 46 | 47 | /* Emit */ 48 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 49 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 50 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 51 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 53 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 63 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 64 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 65 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 66 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 67 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 68 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 69 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 70 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 78 | 79 | /* Type Checking */ 80 | "strict": true /* Enable all strict type-checking options. */, 81 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 82 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 87 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entryPoints: ['src/index.ts'], 5 | }); 6 | --------------------------------------------------------------------------------