├── .env ├── .gitignore ├── LICENSE ├── README.md ├── account_cache.db ├── app.js ├── package-lock.json ├── package.json └── src ├── Achievements └── Achievements.js ├── Backpack ├── Authenticated │ ├── Account.js │ ├── Authentication.js │ ├── BorrowLend.js │ ├── Capital.js │ ├── Futures.js │ ├── History.js │ └── Order.js └── Public │ ├── Assets.js │ ├── BorrowLend.js │ ├── Markets.js │ ├── System.js │ └── Trades.js ├── Controllers ├── AccountController.js ├── CacheController.js ├── OrderController.js └── PnlController.js ├── Decision ├── Decision.js └── Indicators.js ├── Grid └── Grid.js ├── TrailingStop ├── StopEvaluator.js └── TrailingStopStream.js └── Utils ├── Terminal.js └── Utils.js /.env: -------------------------------------------------------------------------------- 1 | API_URL=https://api.backpack.exchange 2 | 3 | # BACKPACK KEYS 4 | BACKPACK_API_KEY="BACKPACK_API_KEY=" 5 | BACKPACK_API_SECRET="BACKPACK_API_SECRET" 6 | 7 | 8 | # GLOBAL CONFIGS 9 | VOLUME_BY_POINT=600 10 | PREVIEW_FARM_LAST_HOURS=24 11 | LOG="true" 12 | TRADING_STRATEGY="DEFAULT" 13 | 14 | 15 | # MODO DEFAULT 16 | AUTHORIZED_MARKET='[]' 17 | CERTAINTY=75 18 | VOLUME_ORDER=250 19 | ENABLE_STOPLOSS="true" 20 | MAX_ORDER_OPEN=10 21 | UNIQUE_TREND="LONG" #LONG, SHORT OR "" TO IGNORE 22 | 23 | 24 | # MODO AUTOMATIC_STOP 25 | TRAILING_STOP_GAP=1 26 | MAX_PERCENT_LOSS = "2%" 27 | MAX_PERCENT_PROFIT = "2%" 28 | 29 | 30 | # MODO GRID 31 | GRID_MARKET="SOL_USDC_PERP" 32 | NUMBER_OF_GRIDS=50 33 | UPPER_PRICE=186 34 | LOWER_PRICE=179 35 | UPPER_FORCE_CLOSE=185.5 36 | LOWER_FORCE_CLOSE=177 37 | GRID_PNL=10 38 | 39 | 40 | #TO USE IN FUTURE: 41 | 42 | # MODO LOOP_HEDGE 43 | #HEDGE_MARKET="PUMP_USDC_PERP" 44 | 45 | # MODE ACHIEVEMENTS 46 | #SPOT_MARKET="ES_USDC" 47 | #FRONT_RUNING=true 48 | #START_UTC="2025-07-16T04:00:00" 49 | #GOAL_VOLUME=10000 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependências 2 | node_modules/ 3 | 4 | # build 5 | dist/ 6 | build/ 7 | 8 | # configurações do sistema 9 | .DS_Store 10 | Thumbs.db 11 | 12 | # ambiente 13 | .env 14 | 15 | # logs 16 | logs/ 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # editor 23 | .vscode/ 24 | .idea/ 25 | 26 | # PM2 27 | pids/ 28 | *.pid 29 | *.seed 30 | 31 | # coverage 32 | coverage/ 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Heron Jr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backbot 2 | A crypto trading bot for Backpack Exchange. It trades perpetual futures automatically using custom strategies and real-time market data. 3 | Use at your own risk – bugs may exist, and the logic won't always yield profits. 4 | But if you know what you're doing, it might save you some time. 5 | 6 | # Steps 7 | In order to run the script you need to: 8 | 9 | * Install nodejs - https://nodejs.org/pt/download 10 | * Create an subaccount in backpack exclusive for bot (low fund for risk) 11 | * Create API Key for backpack exchange subaccount 12 | * Configure the file .env with your setup, Save file. 13 | * Run in terminal npm start 14 | 15 | # Configs 16 | 17 | ## Global Configs 18 | 19 | * `BACKPACK_API_KEY` Your Backpack Exchange API key. 20 | * `BACKPACK_API_SECRET` Your Backpack Exchange API secret. 21 | * `VOLUME_BY_POINT` Your average volume per point on Backpack. For example: `600`. The bot will understand that for every $600 traded, 1 point is gained, estimating progress. 22 | * `PREVIEW_FARM_LAST_HOURS` Number of past hours to preview bot activity. For example: `5` will return the last 5 hours of performance. 23 | * `LOG` Enables visible logs in the terminal when set to `true`; disables when `false`. 24 | * `TRADING_STRATEGY` Choose your preferred strategy. Available options: `DEFAULT`, `AUTOMATIC_STOP`, and `GRID`. 25 | 26 | --- 27 | 28 | ## 1. `DEFAULT` MODE 29 | 30 | Find market opportunities and open limit long or short positions with stop loss and take profit. Stop loss can be enabled optionally. 31 | 32 | * `AUTHORIZED_MARKET` Markets allowed for trading. If set to `'[]'`, all markets will be used. To restrict to specific markets, use something like: 33 | `'["BTC_USDC_PERP", "SOL_USDC_PERP", "ETH_USDC_PERP"]'` 34 | * `CERTAINTY` Minimum certainty level required by the algorithm to open an order. Higher values will result in fewer trades. Range: `0 to 100`. 35 | * `VOLUME_ORDER` Default order volume in USD. 36 | * `ENABLE_STOPLOSS` Enables stop loss from the `AUTOMATIC_STOP` module when set to `true`; disables when `false`. Can be used together with `DEFAULT`. 37 | * `MAX_ORDER_OPEN` Maximum number of orders that can be open simultaneously (includes untriggered orders). 38 | * `UNIQUE_TREND` If set to `''`, both LONG and SHORT positions are allowed. To restrict only LONG, use `"LONG"`; for SHORT only, use `"SHORT"`. 39 | 40 | --- 41 | 42 | ## 2. `AUTOMATIC_STOP` MODE 43 | 44 | Monitors open orders and attempts to update stop losses based on profit gaps. 45 | 46 | * `TRAILING_STOP_GAP` Minimum acceptable gap in USD between the current stop loss and the next one. Example: If profit increases by `$1`, the stop will be updated. 47 | * `MAX_PERCENT_LOSS` Maximum acceptable loss percentage, e.g., `1%`. This is calculated based on order volume — be cautious when using leverage. 48 | * `MAX_PERCENT_PROFIT` Maximum profit target percentage, e.g., `2%`. Also based on order volume — leverage applies. 49 | 50 | --- 51 | 52 | ## 3. `GRID` MODE 53 | 54 | Places orders above and below the current price to create volume in sideways markets. 55 | 56 | * `GRID_MARKET` 57 | Market pair to be used in grid mode. 58 | Example: `"SOL_USDC_PERP"` 59 | 60 | * `NUMBER_OF_GRIDS` 61 | Total number of grid levels the bot will create between `LOWER_PRICE` and `UPPER_PRICE`. 62 | Example: `100` creates 100 evenly spaced orders within the defined price range. 63 | 64 | * `UPPER_PRICE` 65 | Highest price where the grid will place orders. No orders will be created above this value. 66 | Example: `185` 67 | 68 | * `LOWER_PRICE` 69 | Lowest price where the grid will place orders. No orders will be created below this value. 70 | Example: `179` 71 | 72 | * `UPPER_FORCE_CLOSE` 73 | If the market price reaches or exceeds this value, all open grid positions will be force-closed (top trigger). 74 | Example: `185.5` 75 | 76 | * `LOWER_FORCE_CLOSE` 77 | If the market price hits or drops below this value, all open grid positions will be force-closed (bottom trigger). 78 | Example: `178` 79 | 80 | * `GRID_PNL` 81 | Estimated profit target for the grid. Once reached, all positions are closed at market and the grid is rebuilt. 82 | Example: `10` 83 | 84 | 85 | ```shell 86 | npm install 87 | npm start 88 | ``` 89 | # Honorable Mention 90 | 91 | - **[@MBFC24](https://x.com/MBFC24)** 92 | Suggested a strategy using neutral delta with collateral in the native token. I’m considering integrating this with real-time funding rates to enable shorting — this will be implemented soon as the `LOOP_HEDGE` mode. 93 | 94 | - **[@Coleta_Cripto](https://x.com/Coleta_Cripto)** 95 | Suggested a mode to simplify the process of earning Backpack’s `Achievements`. Coming soon! 96 | 97 | - **[pordria](https://github.com/pordria)** 98 | Created a Backpack grid bot. This project is based on their logic. 99 | 100 | - **[@owvituh](https://x.com/owvituh) - [GitHub](https://github.com/OvictorVieira/backbot)** 101 | Forked my bot and added multi-account and multi-strategy support. I’ve taken several ideas from it for this version. 102 | 103 | --- 104 | 105 | # Coming Soon 106 | 107 | 1. `LOOP_HEDGE` Mode – Neutral delta with funding-based shorting 108 | 2. `Achievements` Mode – Automates collection of Backpack achievements 109 | 3. `FRONT RUN` Mode – Reacts to new token listings in real-time 110 | 111 | --- 112 | 113 | # Sponsor 114 | 115 | If this bot has helped you, consider buying me a coffee! 116 | 117 | **SOL Address:** 118 | `8MeRfGewLeU419PDPW9HGzM9Aqf79yh7uCXZFLbjAg5a` 119 | -------------------------------------------------------------------------------- /account_cache.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heron-jr/backbot/ae514b345ef3168a26ad584e5bd4f3cfec09bc88/account_cache.db -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import Decision from './src/Decision/Decision.js'; 3 | import PnlController from './src/Controllers/PnlController.js'; 4 | import TrailingStopStream from './src/TrailingStop/TrailingStopStream.js' 5 | import CacheController from './src/Controllers/CacheController.js'; 6 | import Grid from './src/Grid/Grid.js'; 7 | import Achievements from './src/Achievements/Achievements.js'; 8 | import Futures from './src/Backpack/Authenticated/Futures.js'; 9 | 10 | const Cache = new CacheController(); 11 | 12 | dotenv.config(); 13 | 14 | const TRADING_STRATEGY = process.env.TRADING_STRATEGY 15 | const PREVIEW_FARM_LAST_HOURS = process.env.PREVIEW_FARM_LAST_HOURS 16 | 17 | await PnlController.start(TRADING_STRATEGY) 18 | 19 | if(PREVIEW_FARM_LAST_HOURS){ 20 | await PnlController.run(Number(PREVIEW_FARM_LAST_HOURS)) 21 | } 22 | 23 | await Cache.update(); 24 | 25 | await new Promise(resolve => setTimeout(resolve, 5000)); 26 | 27 | 28 | async function startDecision() { 29 | await Decision.analyze(); 30 | setTimeout(startDecision, 1000 * 60); 31 | } 32 | 33 | if(TRADING_STRATEGY === "DEFAULT") { 34 | startDecision() 35 | 36 | const enableStopLoss = String(process.env.ENABLE_STOPLOSS).toUpperCase() === "TRUE" 37 | if(enableStopLoss) { 38 | TrailingStopStream.start(); 39 | } 40 | 41 | } 42 | 43 | if(TRADING_STRATEGY === "AUTOMATIC_STOP") { 44 | TrailingStopStream.start(); 45 | } 46 | 47 | if(TRADING_STRATEGY === "GRID") { 48 | Grid.run() 49 | } 50 | 51 | if(TRADING_STRATEGY === "HEDGE_MARKET"){ 52 | console.log("🐋 Don't be hasty, it's coming in the next version. Spoilers in the code.") 53 | } 54 | 55 | if(TRADING_STRATEGY === "ACHIEVEMENTS"){ 56 | console.log("🐋 Don't be hasty, it's coming in the next version. Spoilers in the code.") 57 | } 58 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbot", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "backbot", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "axios": "^1.8.4", 13 | "bs58": "^6.0.0", 14 | "cron": "^4.3.0", 15 | "dotenv": "^16.5.0", 16 | "nodemon": "^3.1.9", 17 | "openai": "^5.3.0", 18 | "sqlite": "^5.1.1", 19 | "sqlite3": "^5.1.7", 20 | "technicalindicators": "^3.1.0", 21 | "tweetnacl": "^1.0.3", 22 | "ws": "^8.18.1" 23 | } 24 | }, 25 | "node_modules/@gar/promisify": { 26 | "version": "1.1.3", 27 | "license": "MIT", 28 | "optional": true 29 | }, 30 | "node_modules/@npmcli/fs": { 31 | "version": "1.1.1", 32 | "license": "ISC", 33 | "optional": true, 34 | "dependencies": { 35 | "@gar/promisify": "^1.0.1", 36 | "semver": "^7.3.5" 37 | } 38 | }, 39 | "node_modules/@npmcli/move-file": { 40 | "version": "1.1.2", 41 | "license": "MIT", 42 | "optional": true, 43 | "dependencies": { 44 | "mkdirp": "^1.0.4", 45 | "rimraf": "^3.0.2" 46 | }, 47 | "engines": { 48 | "node": ">=10" 49 | } 50 | }, 51 | "node_modules/@tootallnate/once": { 52 | "version": "1.1.2", 53 | "license": "MIT", 54 | "optional": true, 55 | "engines": { 56 | "node": ">= 6" 57 | } 58 | }, 59 | "node_modules/@types/luxon": { 60 | "version": "3.6.2", 61 | "license": "MIT" 62 | }, 63 | "node_modules/@types/node": { 64 | "version": "6.14.13", 65 | "license": "MIT" 66 | }, 67 | "node_modules/abbrev": { 68 | "version": "1.1.1", 69 | "license": "ISC", 70 | "optional": true 71 | }, 72 | "node_modules/agent-base": { 73 | "version": "6.0.2", 74 | "license": "MIT", 75 | "optional": true, 76 | "dependencies": { 77 | "debug": "4" 78 | }, 79 | "engines": { 80 | "node": ">= 6.0.0" 81 | } 82 | }, 83 | "node_modules/agentkeepalive": { 84 | "version": "4.6.0", 85 | "license": "MIT", 86 | "optional": true, 87 | "dependencies": { 88 | "humanize-ms": "^1.2.1" 89 | }, 90 | "engines": { 91 | "node": ">= 8.0.0" 92 | } 93 | }, 94 | "node_modules/aggregate-error": { 95 | "version": "3.1.0", 96 | "license": "MIT", 97 | "optional": true, 98 | "dependencies": { 99 | "clean-stack": "^2.0.0", 100 | "indent-string": "^4.0.0" 101 | }, 102 | "engines": { 103 | "node": ">=8" 104 | } 105 | }, 106 | "node_modules/ansi-regex": { 107 | "version": "5.0.1", 108 | "license": "MIT", 109 | "optional": true, 110 | "engines": { 111 | "node": ">=8" 112 | } 113 | }, 114 | "node_modules/anymatch": { 115 | "version": "3.1.3", 116 | "license": "ISC", 117 | "dependencies": { 118 | "normalize-path": "^3.0.0", 119 | "picomatch": "^2.0.4" 120 | }, 121 | "engines": { 122 | "node": ">= 8" 123 | } 124 | }, 125 | "node_modules/aproba": { 126 | "version": "2.0.0", 127 | "license": "ISC", 128 | "optional": true 129 | }, 130 | "node_modules/are-we-there-yet": { 131 | "version": "3.0.1", 132 | "license": "ISC", 133 | "optional": true, 134 | "dependencies": { 135 | "delegates": "^1.0.0", 136 | "readable-stream": "^3.6.0" 137 | }, 138 | "engines": { 139 | "node": "^12.13.0 || ^14.15.0 || >=16.0.0" 140 | } 141 | }, 142 | "node_modules/asynckit": { 143 | "version": "0.4.0", 144 | "license": "MIT" 145 | }, 146 | "node_modules/axios": { 147 | "version": "1.8.4", 148 | "license": "MIT", 149 | "dependencies": { 150 | "follow-redirects": "^1.15.6", 151 | "form-data": "^4.0.0", 152 | "proxy-from-env": "^1.1.0" 153 | } 154 | }, 155 | "node_modules/balanced-match": { 156 | "version": "1.0.2", 157 | "license": "MIT" 158 | }, 159 | "node_modules/base-x": { 160 | "version": "5.0.1", 161 | "license": "MIT" 162 | }, 163 | "node_modules/base64-js": { 164 | "version": "1.5.1", 165 | "funding": [ 166 | { 167 | "type": "github", 168 | "url": "https://github.com/sponsors/feross" 169 | }, 170 | { 171 | "type": "patreon", 172 | "url": "https://www.patreon.com/feross" 173 | }, 174 | { 175 | "type": "consulting", 176 | "url": "https://feross.org/support" 177 | } 178 | ], 179 | "license": "MIT" 180 | }, 181 | "node_modules/binary-extensions": { 182 | "version": "2.3.0", 183 | "license": "MIT", 184 | "engines": { 185 | "node": ">=8" 186 | }, 187 | "funding": { 188 | "url": "https://github.com/sponsors/sindresorhus" 189 | } 190 | }, 191 | "node_modules/bindings": { 192 | "version": "1.5.0", 193 | "license": "MIT", 194 | "dependencies": { 195 | "file-uri-to-path": "1.0.0" 196 | } 197 | }, 198 | "node_modules/bl": { 199 | "version": "4.1.0", 200 | "license": "MIT", 201 | "dependencies": { 202 | "buffer": "^5.5.0", 203 | "inherits": "^2.0.4", 204 | "readable-stream": "^3.4.0" 205 | } 206 | }, 207 | "node_modules/brace-expansion": { 208 | "version": "1.1.12", 209 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 210 | "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 211 | "dependencies": { 212 | "balanced-match": "^1.0.0", 213 | "concat-map": "0.0.1" 214 | } 215 | }, 216 | "node_modules/braces": { 217 | "version": "3.0.3", 218 | "license": "MIT", 219 | "dependencies": { 220 | "fill-range": "^7.1.1" 221 | }, 222 | "engines": { 223 | "node": ">=8" 224 | } 225 | }, 226 | "node_modules/bs58": { 227 | "version": "6.0.0", 228 | "license": "MIT", 229 | "dependencies": { 230 | "base-x": "^5.0.0" 231 | } 232 | }, 233 | "node_modules/buffer": { 234 | "version": "5.7.1", 235 | "funding": [ 236 | { 237 | "type": "github", 238 | "url": "https://github.com/sponsors/feross" 239 | }, 240 | { 241 | "type": "patreon", 242 | "url": "https://www.patreon.com/feross" 243 | }, 244 | { 245 | "type": "consulting", 246 | "url": "https://feross.org/support" 247 | } 248 | ], 249 | "license": "MIT", 250 | "dependencies": { 251 | "base64-js": "^1.3.1", 252 | "ieee754": "^1.1.13" 253 | } 254 | }, 255 | "node_modules/cacache": { 256 | "version": "15.3.0", 257 | "license": "ISC", 258 | "optional": true, 259 | "dependencies": { 260 | "@npmcli/fs": "^1.0.0", 261 | "@npmcli/move-file": "^1.0.1", 262 | "chownr": "^2.0.0", 263 | "fs-minipass": "^2.0.0", 264 | "glob": "^7.1.4", 265 | "infer-owner": "^1.0.4", 266 | "lru-cache": "^6.0.0", 267 | "minipass": "^3.1.1", 268 | "minipass-collect": "^1.0.2", 269 | "minipass-flush": "^1.0.5", 270 | "minipass-pipeline": "^1.2.2", 271 | "mkdirp": "^1.0.3", 272 | "p-map": "^4.0.0", 273 | "promise-inflight": "^1.0.1", 274 | "rimraf": "^3.0.2", 275 | "ssri": "^8.0.1", 276 | "tar": "^6.0.2", 277 | "unique-filename": "^1.1.1" 278 | }, 279 | "engines": { 280 | "node": ">= 10" 281 | } 282 | }, 283 | "node_modules/call-bind-apply-helpers": { 284 | "version": "1.0.2", 285 | "license": "MIT", 286 | "dependencies": { 287 | "es-errors": "^1.3.0", 288 | "function-bind": "^1.1.2" 289 | }, 290 | "engines": { 291 | "node": ">= 0.4" 292 | } 293 | }, 294 | "node_modules/chokidar": { 295 | "version": "3.6.0", 296 | "license": "MIT", 297 | "dependencies": { 298 | "anymatch": "~3.1.2", 299 | "braces": "~3.0.2", 300 | "glob-parent": "~5.1.2", 301 | "is-binary-path": "~2.1.0", 302 | "is-glob": "~4.0.1", 303 | "normalize-path": "~3.0.0", 304 | "readdirp": "~3.6.0" 305 | }, 306 | "engines": { 307 | "node": ">= 8.10.0" 308 | }, 309 | "funding": { 310 | "url": "https://paulmillr.com/funding/" 311 | }, 312 | "optionalDependencies": { 313 | "fsevents": "~2.3.2" 314 | } 315 | }, 316 | "node_modules/chownr": { 317 | "version": "2.0.0", 318 | "license": "ISC", 319 | "engines": { 320 | "node": ">=10" 321 | } 322 | }, 323 | "node_modules/clean-stack": { 324 | "version": "2.2.0", 325 | "license": "MIT", 326 | "optional": true, 327 | "engines": { 328 | "node": ">=6" 329 | } 330 | }, 331 | "node_modules/color-support": { 332 | "version": "1.1.3", 333 | "license": "ISC", 334 | "optional": true, 335 | "bin": { 336 | "color-support": "bin.js" 337 | } 338 | }, 339 | "node_modules/combined-stream": { 340 | "version": "1.0.8", 341 | "license": "MIT", 342 | "dependencies": { 343 | "delayed-stream": "~1.0.0" 344 | }, 345 | "engines": { 346 | "node": ">= 0.8" 347 | } 348 | }, 349 | "node_modules/concat-map": { 350 | "version": "0.0.1", 351 | "license": "MIT" 352 | }, 353 | "node_modules/console-control-strings": { 354 | "version": "1.1.0", 355 | "license": "ISC", 356 | "optional": true 357 | }, 358 | "node_modules/cron": { 359 | "version": "4.3.0", 360 | "license": "MIT", 361 | "dependencies": { 362 | "@types/luxon": "~3.6.0", 363 | "luxon": "~3.6.0" 364 | }, 365 | "engines": { 366 | "node": ">=18.x" 367 | } 368 | }, 369 | "node_modules/debug": { 370 | "version": "4.4.0", 371 | "license": "MIT", 372 | "dependencies": { 373 | "ms": "^2.1.3" 374 | }, 375 | "engines": { 376 | "node": ">=6.0" 377 | }, 378 | "peerDependenciesMeta": { 379 | "supports-color": { 380 | "optional": true 381 | } 382 | } 383 | }, 384 | "node_modules/decompress-response": { 385 | "version": "6.0.0", 386 | "license": "MIT", 387 | "dependencies": { 388 | "mimic-response": "^3.1.0" 389 | }, 390 | "engines": { 391 | "node": ">=10" 392 | }, 393 | "funding": { 394 | "url": "https://github.com/sponsors/sindresorhus" 395 | } 396 | }, 397 | "node_modules/deep-extend": { 398 | "version": "0.6.0", 399 | "license": "MIT", 400 | "engines": { 401 | "node": ">=4.0.0" 402 | } 403 | }, 404 | "node_modules/delayed-stream": { 405 | "version": "1.0.0", 406 | "license": "MIT", 407 | "engines": { 408 | "node": ">=0.4.0" 409 | } 410 | }, 411 | "node_modules/delegates": { 412 | "version": "1.0.0", 413 | "license": "MIT", 414 | "optional": true 415 | }, 416 | "node_modules/detect-libc": { 417 | "version": "2.0.3", 418 | "license": "Apache-2.0", 419 | "engines": { 420 | "node": ">=8" 421 | } 422 | }, 423 | "node_modules/dotenv": { 424 | "version": "16.5.0", 425 | "license": "BSD-2-Clause", 426 | "engines": { 427 | "node": ">=12" 428 | }, 429 | "funding": { 430 | "url": "https://dotenvx.com" 431 | } 432 | }, 433 | "node_modules/dunder-proto": { 434 | "version": "1.0.1", 435 | "license": "MIT", 436 | "dependencies": { 437 | "call-bind-apply-helpers": "^1.0.1", 438 | "es-errors": "^1.3.0", 439 | "gopd": "^1.2.0" 440 | }, 441 | "engines": { 442 | "node": ">= 0.4" 443 | } 444 | }, 445 | "node_modules/emoji-regex": { 446 | "version": "8.0.0", 447 | "license": "MIT", 448 | "optional": true 449 | }, 450 | "node_modules/encoding": { 451 | "version": "0.1.13", 452 | "license": "MIT", 453 | "optional": true, 454 | "dependencies": { 455 | "iconv-lite": "^0.6.2" 456 | } 457 | }, 458 | "node_modules/end-of-stream": { 459 | "version": "1.4.4", 460 | "license": "MIT", 461 | "dependencies": { 462 | "once": "^1.4.0" 463 | } 464 | }, 465 | "node_modules/env-paths": { 466 | "version": "2.2.1", 467 | "license": "MIT", 468 | "optional": true, 469 | "engines": { 470 | "node": ">=6" 471 | } 472 | }, 473 | "node_modules/err-code": { 474 | "version": "2.0.3", 475 | "license": "MIT", 476 | "optional": true 477 | }, 478 | "node_modules/es-define-property": { 479 | "version": "1.0.1", 480 | "license": "MIT", 481 | "engines": { 482 | "node": ">= 0.4" 483 | } 484 | }, 485 | "node_modules/es-errors": { 486 | "version": "1.3.0", 487 | "license": "MIT", 488 | "engines": { 489 | "node": ">= 0.4" 490 | } 491 | }, 492 | "node_modules/es-object-atoms": { 493 | "version": "1.1.1", 494 | "license": "MIT", 495 | "dependencies": { 496 | "es-errors": "^1.3.0" 497 | }, 498 | "engines": { 499 | "node": ">= 0.4" 500 | } 501 | }, 502 | "node_modules/es-set-tostringtag": { 503 | "version": "2.1.0", 504 | "license": "MIT", 505 | "dependencies": { 506 | "es-errors": "^1.3.0", 507 | "get-intrinsic": "^1.2.6", 508 | "has-tostringtag": "^1.0.2", 509 | "hasown": "^2.0.2" 510 | }, 511 | "engines": { 512 | "node": ">= 0.4" 513 | } 514 | }, 515 | "node_modules/expand-template": { 516 | "version": "2.0.3", 517 | "license": "(MIT OR WTFPL)", 518 | "engines": { 519 | "node": ">=6" 520 | } 521 | }, 522 | "node_modules/file-uri-to-path": { 523 | "version": "1.0.0", 524 | "license": "MIT" 525 | }, 526 | "node_modules/fill-range": { 527 | "version": "7.1.1", 528 | "license": "MIT", 529 | "dependencies": { 530 | "to-regex-range": "^5.0.1" 531 | }, 532 | "engines": { 533 | "node": ">=8" 534 | } 535 | }, 536 | "node_modules/follow-redirects": { 537 | "version": "1.15.9", 538 | "funding": [ 539 | { 540 | "type": "individual", 541 | "url": "https://github.com/sponsors/RubenVerborgh" 542 | } 543 | ], 544 | "license": "MIT", 545 | "engines": { 546 | "node": ">=4.0" 547 | }, 548 | "peerDependenciesMeta": { 549 | "debug": { 550 | "optional": true 551 | } 552 | } 553 | }, 554 | "node_modules/form-data": { 555 | "version": "4.0.2", 556 | "license": "MIT", 557 | "dependencies": { 558 | "asynckit": "^0.4.0", 559 | "combined-stream": "^1.0.8", 560 | "es-set-tostringtag": "^2.1.0", 561 | "mime-types": "^2.1.12" 562 | }, 563 | "engines": { 564 | "node": ">= 6" 565 | } 566 | }, 567 | "node_modules/fs-constants": { 568 | "version": "1.0.0", 569 | "license": "MIT" 570 | }, 571 | "node_modules/fs-minipass": { 572 | "version": "2.1.0", 573 | "license": "ISC", 574 | "dependencies": { 575 | "minipass": "^3.0.0" 576 | }, 577 | "engines": { 578 | "node": ">= 8" 579 | } 580 | }, 581 | "node_modules/fs.realpath": { 582 | "version": "1.0.0", 583 | "license": "ISC", 584 | "optional": true 585 | }, 586 | "node_modules/function-bind": { 587 | "version": "1.1.2", 588 | "license": "MIT", 589 | "funding": { 590 | "url": "https://github.com/sponsors/ljharb" 591 | } 592 | }, 593 | "node_modules/gauge": { 594 | "version": "4.0.4", 595 | "license": "ISC", 596 | "optional": true, 597 | "dependencies": { 598 | "aproba": "^1.0.3 || ^2.0.0", 599 | "color-support": "^1.1.3", 600 | "console-control-strings": "^1.1.0", 601 | "has-unicode": "^2.0.1", 602 | "signal-exit": "^3.0.7", 603 | "string-width": "^4.2.3", 604 | "strip-ansi": "^6.0.1", 605 | "wide-align": "^1.1.5" 606 | }, 607 | "engines": { 608 | "node": "^12.13.0 || ^14.15.0 || >=16.0.0" 609 | } 610 | }, 611 | "node_modules/get-intrinsic": { 612 | "version": "1.3.0", 613 | "license": "MIT", 614 | "dependencies": { 615 | "call-bind-apply-helpers": "^1.0.2", 616 | "es-define-property": "^1.0.1", 617 | "es-errors": "^1.3.0", 618 | "es-object-atoms": "^1.1.1", 619 | "function-bind": "^1.1.2", 620 | "get-proto": "^1.0.1", 621 | "gopd": "^1.2.0", 622 | "has-symbols": "^1.1.0", 623 | "hasown": "^2.0.2", 624 | "math-intrinsics": "^1.1.0" 625 | }, 626 | "engines": { 627 | "node": ">= 0.4" 628 | }, 629 | "funding": { 630 | "url": "https://github.com/sponsors/ljharb" 631 | } 632 | }, 633 | "node_modules/get-proto": { 634 | "version": "1.0.1", 635 | "license": "MIT", 636 | "dependencies": { 637 | "dunder-proto": "^1.0.1", 638 | "es-object-atoms": "^1.0.0" 639 | }, 640 | "engines": { 641 | "node": ">= 0.4" 642 | } 643 | }, 644 | "node_modules/github-from-package": { 645 | "version": "0.0.0", 646 | "license": "MIT" 647 | }, 648 | "node_modules/glob": { 649 | "version": "7.2.3", 650 | "license": "ISC", 651 | "optional": true, 652 | "dependencies": { 653 | "fs.realpath": "^1.0.0", 654 | "inflight": "^1.0.4", 655 | "inherits": "2", 656 | "minimatch": "^3.1.1", 657 | "once": "^1.3.0", 658 | "path-is-absolute": "^1.0.0" 659 | }, 660 | "engines": { 661 | "node": "*" 662 | }, 663 | "funding": { 664 | "url": "https://github.com/sponsors/isaacs" 665 | } 666 | }, 667 | "node_modules/glob-parent": { 668 | "version": "5.1.2", 669 | "license": "ISC", 670 | "dependencies": { 671 | "is-glob": "^4.0.1" 672 | }, 673 | "engines": { 674 | "node": ">= 6" 675 | } 676 | }, 677 | "node_modules/gopd": { 678 | "version": "1.2.0", 679 | "license": "MIT", 680 | "engines": { 681 | "node": ">= 0.4" 682 | }, 683 | "funding": { 684 | "url": "https://github.com/sponsors/ljharb" 685 | } 686 | }, 687 | "node_modules/graceful-fs": { 688 | "version": "4.2.11", 689 | "license": "ISC", 690 | "optional": true 691 | }, 692 | "node_modules/has-flag": { 693 | "version": "3.0.0", 694 | "license": "MIT", 695 | "engines": { 696 | "node": ">=4" 697 | } 698 | }, 699 | "node_modules/has-symbols": { 700 | "version": "1.1.0", 701 | "license": "MIT", 702 | "engines": { 703 | "node": ">= 0.4" 704 | }, 705 | "funding": { 706 | "url": "https://github.com/sponsors/ljharb" 707 | } 708 | }, 709 | "node_modules/has-tostringtag": { 710 | "version": "1.0.2", 711 | "license": "MIT", 712 | "dependencies": { 713 | "has-symbols": "^1.0.3" 714 | }, 715 | "engines": { 716 | "node": ">= 0.4" 717 | }, 718 | "funding": { 719 | "url": "https://github.com/sponsors/ljharb" 720 | } 721 | }, 722 | "node_modules/has-unicode": { 723 | "version": "2.0.1", 724 | "license": "ISC", 725 | "optional": true 726 | }, 727 | "node_modules/hasown": { 728 | "version": "2.0.2", 729 | "license": "MIT", 730 | "dependencies": { 731 | "function-bind": "^1.1.2" 732 | }, 733 | "engines": { 734 | "node": ">= 0.4" 735 | } 736 | }, 737 | "node_modules/http-cache-semantics": { 738 | "version": "4.1.1", 739 | "license": "BSD-2-Clause", 740 | "optional": true 741 | }, 742 | "node_modules/http-proxy-agent": { 743 | "version": "4.0.1", 744 | "license": "MIT", 745 | "optional": true, 746 | "dependencies": { 747 | "@tootallnate/once": "1", 748 | "agent-base": "6", 749 | "debug": "4" 750 | }, 751 | "engines": { 752 | "node": ">= 6" 753 | } 754 | }, 755 | "node_modules/https-proxy-agent": { 756 | "version": "5.0.1", 757 | "license": "MIT", 758 | "optional": true, 759 | "dependencies": { 760 | "agent-base": "6", 761 | "debug": "4" 762 | }, 763 | "engines": { 764 | "node": ">= 6" 765 | } 766 | }, 767 | "node_modules/humanize-ms": { 768 | "version": "1.2.1", 769 | "license": "MIT", 770 | "optional": true, 771 | "dependencies": { 772 | "ms": "^2.0.0" 773 | } 774 | }, 775 | "node_modules/iconv-lite": { 776 | "version": "0.6.3", 777 | "license": "MIT", 778 | "optional": true, 779 | "dependencies": { 780 | "safer-buffer": ">= 2.1.2 < 3.0.0" 781 | }, 782 | "engines": { 783 | "node": ">=0.10.0" 784 | } 785 | }, 786 | "node_modules/ieee754": { 787 | "version": "1.2.1", 788 | "funding": [ 789 | { 790 | "type": "github", 791 | "url": "https://github.com/sponsors/feross" 792 | }, 793 | { 794 | "type": "patreon", 795 | "url": "https://www.patreon.com/feross" 796 | }, 797 | { 798 | "type": "consulting", 799 | "url": "https://feross.org/support" 800 | } 801 | ], 802 | "license": "BSD-3-Clause" 803 | }, 804 | "node_modules/ignore-by-default": { 805 | "version": "1.0.1", 806 | "license": "ISC" 807 | }, 808 | "node_modules/imurmurhash": { 809 | "version": "0.1.4", 810 | "license": "MIT", 811 | "optional": true, 812 | "engines": { 813 | "node": ">=0.8.19" 814 | } 815 | }, 816 | "node_modules/indent-string": { 817 | "version": "4.0.0", 818 | "license": "MIT", 819 | "optional": true, 820 | "engines": { 821 | "node": ">=8" 822 | } 823 | }, 824 | "node_modules/infer-owner": { 825 | "version": "1.0.4", 826 | "license": "ISC", 827 | "optional": true 828 | }, 829 | "node_modules/inflight": { 830 | "version": "1.0.6", 831 | "license": "ISC", 832 | "optional": true, 833 | "dependencies": { 834 | "once": "^1.3.0", 835 | "wrappy": "1" 836 | } 837 | }, 838 | "node_modules/inherits": { 839 | "version": "2.0.4", 840 | "license": "ISC" 841 | }, 842 | "node_modules/ini": { 843 | "version": "1.3.8", 844 | "license": "ISC" 845 | }, 846 | "node_modules/ip-address": { 847 | "version": "9.0.5", 848 | "license": "MIT", 849 | "optional": true, 850 | "dependencies": { 851 | "jsbn": "1.1.0", 852 | "sprintf-js": "^1.1.3" 853 | }, 854 | "engines": { 855 | "node": ">= 12" 856 | } 857 | }, 858 | "node_modules/is-binary-path": { 859 | "version": "2.1.0", 860 | "license": "MIT", 861 | "dependencies": { 862 | "binary-extensions": "^2.0.0" 863 | }, 864 | "engines": { 865 | "node": ">=8" 866 | } 867 | }, 868 | "node_modules/is-extglob": { 869 | "version": "2.1.1", 870 | "license": "MIT", 871 | "engines": { 872 | "node": ">=0.10.0" 873 | } 874 | }, 875 | "node_modules/is-fullwidth-code-point": { 876 | "version": "3.0.0", 877 | "license": "MIT", 878 | "optional": true, 879 | "engines": { 880 | "node": ">=8" 881 | } 882 | }, 883 | "node_modules/is-glob": { 884 | "version": "4.0.3", 885 | "license": "MIT", 886 | "dependencies": { 887 | "is-extglob": "^2.1.1" 888 | }, 889 | "engines": { 890 | "node": ">=0.10.0" 891 | } 892 | }, 893 | "node_modules/is-lambda": { 894 | "version": "1.0.1", 895 | "license": "MIT", 896 | "optional": true 897 | }, 898 | "node_modules/is-number": { 899 | "version": "7.0.0", 900 | "license": "MIT", 901 | "engines": { 902 | "node": ">=0.12.0" 903 | } 904 | }, 905 | "node_modules/isexe": { 906 | "version": "2.0.0", 907 | "license": "ISC", 908 | "optional": true 909 | }, 910 | "node_modules/jsbn": { 911 | "version": "1.1.0", 912 | "license": "MIT", 913 | "optional": true 914 | }, 915 | "node_modules/lru-cache": { 916 | "version": "6.0.0", 917 | "license": "ISC", 918 | "optional": true, 919 | "dependencies": { 920 | "yallist": "^4.0.0" 921 | }, 922 | "engines": { 923 | "node": ">=10" 924 | } 925 | }, 926 | "node_modules/luxon": { 927 | "version": "3.6.1", 928 | "license": "MIT", 929 | "engines": { 930 | "node": ">=12" 931 | } 932 | }, 933 | "node_modules/make-fetch-happen": { 934 | "version": "9.1.0", 935 | "license": "ISC", 936 | "optional": true, 937 | "dependencies": { 938 | "agentkeepalive": "^4.1.3", 939 | "cacache": "^15.2.0", 940 | "http-cache-semantics": "^4.1.0", 941 | "http-proxy-agent": "^4.0.1", 942 | "https-proxy-agent": "^5.0.0", 943 | "is-lambda": "^1.0.1", 944 | "lru-cache": "^6.0.0", 945 | "minipass": "^3.1.3", 946 | "minipass-collect": "^1.0.2", 947 | "minipass-fetch": "^1.3.2", 948 | "minipass-flush": "^1.0.5", 949 | "minipass-pipeline": "^1.2.4", 950 | "negotiator": "^0.6.2", 951 | "promise-retry": "^2.0.1", 952 | "socks-proxy-agent": "^6.0.0", 953 | "ssri": "^8.0.0" 954 | }, 955 | "engines": { 956 | "node": ">= 10" 957 | } 958 | }, 959 | "node_modules/math-intrinsics": { 960 | "version": "1.1.0", 961 | "license": "MIT", 962 | "engines": { 963 | "node": ">= 0.4" 964 | } 965 | }, 966 | "node_modules/mime-db": { 967 | "version": "1.52.0", 968 | "license": "MIT", 969 | "engines": { 970 | "node": ">= 0.6" 971 | } 972 | }, 973 | "node_modules/mime-types": { 974 | "version": "2.1.35", 975 | "license": "MIT", 976 | "dependencies": { 977 | "mime-db": "1.52.0" 978 | }, 979 | "engines": { 980 | "node": ">= 0.6" 981 | } 982 | }, 983 | "node_modules/mimic-response": { 984 | "version": "3.1.0", 985 | "license": "MIT", 986 | "engines": { 987 | "node": ">=10" 988 | }, 989 | "funding": { 990 | "url": "https://github.com/sponsors/sindresorhus" 991 | } 992 | }, 993 | "node_modules/minimatch": { 994 | "version": "3.1.2", 995 | "license": "ISC", 996 | "dependencies": { 997 | "brace-expansion": "^1.1.7" 998 | }, 999 | "engines": { 1000 | "node": "*" 1001 | } 1002 | }, 1003 | "node_modules/minimist": { 1004 | "version": "1.2.8", 1005 | "license": "MIT", 1006 | "funding": { 1007 | "url": "https://github.com/sponsors/ljharb" 1008 | } 1009 | }, 1010 | "node_modules/minipass": { 1011 | "version": "3.3.6", 1012 | "license": "ISC", 1013 | "dependencies": { 1014 | "yallist": "^4.0.0" 1015 | }, 1016 | "engines": { 1017 | "node": ">=8" 1018 | } 1019 | }, 1020 | "node_modules/minipass-collect": { 1021 | "version": "1.0.2", 1022 | "license": "ISC", 1023 | "optional": true, 1024 | "dependencies": { 1025 | "minipass": "^3.0.0" 1026 | }, 1027 | "engines": { 1028 | "node": ">= 8" 1029 | } 1030 | }, 1031 | "node_modules/minipass-fetch": { 1032 | "version": "1.4.1", 1033 | "license": "MIT", 1034 | "optional": true, 1035 | "dependencies": { 1036 | "minipass": "^3.1.0", 1037 | "minipass-sized": "^1.0.3", 1038 | "minizlib": "^2.0.0" 1039 | }, 1040 | "engines": { 1041 | "node": ">=8" 1042 | }, 1043 | "optionalDependencies": { 1044 | "encoding": "^0.1.12" 1045 | } 1046 | }, 1047 | "node_modules/minipass-flush": { 1048 | "version": "1.0.5", 1049 | "license": "ISC", 1050 | "optional": true, 1051 | "dependencies": { 1052 | "minipass": "^3.0.0" 1053 | }, 1054 | "engines": { 1055 | "node": ">= 8" 1056 | } 1057 | }, 1058 | "node_modules/minipass-pipeline": { 1059 | "version": "1.2.4", 1060 | "license": "ISC", 1061 | "optional": true, 1062 | "dependencies": { 1063 | "minipass": "^3.0.0" 1064 | }, 1065 | "engines": { 1066 | "node": ">=8" 1067 | } 1068 | }, 1069 | "node_modules/minipass-sized": { 1070 | "version": "1.0.3", 1071 | "license": "ISC", 1072 | "optional": true, 1073 | "dependencies": { 1074 | "minipass": "^3.0.0" 1075 | }, 1076 | "engines": { 1077 | "node": ">=8" 1078 | } 1079 | }, 1080 | "node_modules/minizlib": { 1081 | "version": "2.1.2", 1082 | "license": "MIT", 1083 | "dependencies": { 1084 | "minipass": "^3.0.0", 1085 | "yallist": "^4.0.0" 1086 | }, 1087 | "engines": { 1088 | "node": ">= 8" 1089 | } 1090 | }, 1091 | "node_modules/mkdirp": { 1092 | "version": "1.0.4", 1093 | "license": "MIT", 1094 | "bin": { 1095 | "mkdirp": "bin/cmd.js" 1096 | }, 1097 | "engines": { 1098 | "node": ">=10" 1099 | } 1100 | }, 1101 | "node_modules/mkdirp-classic": { 1102 | "version": "0.5.3", 1103 | "license": "MIT" 1104 | }, 1105 | "node_modules/ms": { 1106 | "version": "2.1.3", 1107 | "license": "MIT" 1108 | }, 1109 | "node_modules/napi-build-utils": { 1110 | "version": "2.0.0", 1111 | "license": "MIT" 1112 | }, 1113 | "node_modules/negotiator": { 1114 | "version": "0.6.4", 1115 | "license": "MIT", 1116 | "optional": true, 1117 | "engines": { 1118 | "node": ">= 0.6" 1119 | } 1120 | }, 1121 | "node_modules/node-abi": { 1122 | "version": "3.74.0", 1123 | "license": "MIT", 1124 | "dependencies": { 1125 | "semver": "^7.3.5" 1126 | }, 1127 | "engines": { 1128 | "node": ">=10" 1129 | } 1130 | }, 1131 | "node_modules/node-addon-api": { 1132 | "version": "7.1.1", 1133 | "license": "MIT" 1134 | }, 1135 | "node_modules/node-gyp": { 1136 | "version": "8.4.1", 1137 | "license": "MIT", 1138 | "optional": true, 1139 | "dependencies": { 1140 | "env-paths": "^2.2.0", 1141 | "glob": "^7.1.4", 1142 | "graceful-fs": "^4.2.6", 1143 | "make-fetch-happen": "^9.1.0", 1144 | "nopt": "^5.0.0", 1145 | "npmlog": "^6.0.0", 1146 | "rimraf": "^3.0.2", 1147 | "semver": "^7.3.5", 1148 | "tar": "^6.1.2", 1149 | "which": "^2.0.2" 1150 | }, 1151 | "bin": { 1152 | "node-gyp": "bin/node-gyp.js" 1153 | }, 1154 | "engines": { 1155 | "node": ">= 10.12.0" 1156 | } 1157 | }, 1158 | "node_modules/nodemon": { 1159 | "version": "3.1.9", 1160 | "license": "MIT", 1161 | "dependencies": { 1162 | "chokidar": "^3.5.2", 1163 | "debug": "^4", 1164 | "ignore-by-default": "^1.0.1", 1165 | "minimatch": "^3.1.2", 1166 | "pstree.remy": "^1.1.8", 1167 | "semver": "^7.5.3", 1168 | "simple-update-notifier": "^2.0.0", 1169 | "supports-color": "^5.5.0", 1170 | "touch": "^3.1.0", 1171 | "undefsafe": "^2.0.5" 1172 | }, 1173 | "bin": { 1174 | "nodemon": "bin/nodemon.js" 1175 | }, 1176 | "engines": { 1177 | "node": ">=10" 1178 | }, 1179 | "funding": { 1180 | "type": "opencollective", 1181 | "url": "https://opencollective.com/nodemon" 1182 | } 1183 | }, 1184 | "node_modules/nopt": { 1185 | "version": "5.0.0", 1186 | "license": "ISC", 1187 | "optional": true, 1188 | "dependencies": { 1189 | "abbrev": "1" 1190 | }, 1191 | "bin": { 1192 | "nopt": "bin/nopt.js" 1193 | }, 1194 | "engines": { 1195 | "node": ">=6" 1196 | } 1197 | }, 1198 | "node_modules/normalize-path": { 1199 | "version": "3.0.0", 1200 | "license": "MIT", 1201 | "engines": { 1202 | "node": ">=0.10.0" 1203 | } 1204 | }, 1205 | "node_modules/npmlog": { 1206 | "version": "6.0.2", 1207 | "license": "ISC", 1208 | "optional": true, 1209 | "dependencies": { 1210 | "are-we-there-yet": "^3.0.0", 1211 | "console-control-strings": "^1.1.0", 1212 | "gauge": "^4.0.3", 1213 | "set-blocking": "^2.0.0" 1214 | }, 1215 | "engines": { 1216 | "node": "^12.13.0 || ^14.15.0 || >=16.0.0" 1217 | } 1218 | }, 1219 | "node_modules/once": { 1220 | "version": "1.4.0", 1221 | "license": "ISC", 1222 | "dependencies": { 1223 | "wrappy": "1" 1224 | } 1225 | }, 1226 | "node_modules/openai": { 1227 | "version": "5.3.0", 1228 | "resolved": "https://registry.npmjs.org/openai/-/openai-5.3.0.tgz", 1229 | "integrity": "sha512-VIKmoF7y4oJCDOwP/oHXGzM69+x0dpGFmN9QmYO+uPbLFOmmnwO+x1GbsgUtI+6oraxomGZ566Y421oYVu191w==", 1230 | "bin": { 1231 | "openai": "bin/cli" 1232 | }, 1233 | "peerDependencies": { 1234 | "ws": "^8.18.0", 1235 | "zod": "^3.23.8" 1236 | }, 1237 | "peerDependenciesMeta": { 1238 | "ws": { 1239 | "optional": true 1240 | }, 1241 | "zod": { 1242 | "optional": true 1243 | } 1244 | } 1245 | }, 1246 | "node_modules/p-map": { 1247 | "version": "4.0.0", 1248 | "license": "MIT", 1249 | "optional": true, 1250 | "dependencies": { 1251 | "aggregate-error": "^3.0.0" 1252 | }, 1253 | "engines": { 1254 | "node": ">=10" 1255 | }, 1256 | "funding": { 1257 | "url": "https://github.com/sponsors/sindresorhus" 1258 | } 1259 | }, 1260 | "node_modules/path-is-absolute": { 1261 | "version": "1.0.1", 1262 | "license": "MIT", 1263 | "optional": true, 1264 | "engines": { 1265 | "node": ">=0.10.0" 1266 | } 1267 | }, 1268 | "node_modules/picomatch": { 1269 | "version": "2.3.1", 1270 | "license": "MIT", 1271 | "engines": { 1272 | "node": ">=8.6" 1273 | }, 1274 | "funding": { 1275 | "url": "https://github.com/sponsors/jonschlinkert" 1276 | } 1277 | }, 1278 | "node_modules/prebuild-install": { 1279 | "version": "7.1.3", 1280 | "license": "MIT", 1281 | "dependencies": { 1282 | "detect-libc": "^2.0.0", 1283 | "expand-template": "^2.0.3", 1284 | "github-from-package": "0.0.0", 1285 | "minimist": "^1.2.3", 1286 | "mkdirp-classic": "^0.5.3", 1287 | "napi-build-utils": "^2.0.0", 1288 | "node-abi": "^3.3.0", 1289 | "pump": "^3.0.0", 1290 | "rc": "^1.2.7", 1291 | "simple-get": "^4.0.0", 1292 | "tar-fs": "^2.0.0", 1293 | "tunnel-agent": "^0.6.0" 1294 | }, 1295 | "bin": { 1296 | "prebuild-install": "bin.js" 1297 | }, 1298 | "engines": { 1299 | "node": ">=10" 1300 | } 1301 | }, 1302 | "node_modules/promise-inflight": { 1303 | "version": "1.0.1", 1304 | "license": "ISC", 1305 | "optional": true 1306 | }, 1307 | "node_modules/promise-retry": { 1308 | "version": "2.0.1", 1309 | "license": "MIT", 1310 | "optional": true, 1311 | "dependencies": { 1312 | "err-code": "^2.0.2", 1313 | "retry": "^0.12.0" 1314 | }, 1315 | "engines": { 1316 | "node": ">=10" 1317 | } 1318 | }, 1319 | "node_modules/proxy-from-env": { 1320 | "version": "1.1.0", 1321 | "license": "MIT" 1322 | }, 1323 | "node_modules/pstree.remy": { 1324 | "version": "1.1.8", 1325 | "license": "MIT" 1326 | }, 1327 | "node_modules/pump": { 1328 | "version": "3.0.2", 1329 | "license": "MIT", 1330 | "dependencies": { 1331 | "end-of-stream": "^1.1.0", 1332 | "once": "^1.3.1" 1333 | } 1334 | }, 1335 | "node_modules/rc": { 1336 | "version": "1.2.8", 1337 | "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", 1338 | "dependencies": { 1339 | "deep-extend": "^0.6.0", 1340 | "ini": "~1.3.0", 1341 | "minimist": "^1.2.0", 1342 | "strip-json-comments": "~2.0.1" 1343 | }, 1344 | "bin": { 1345 | "rc": "cli.js" 1346 | } 1347 | }, 1348 | "node_modules/readable-stream": { 1349 | "version": "3.6.2", 1350 | "license": "MIT", 1351 | "dependencies": { 1352 | "inherits": "^2.0.3", 1353 | "string_decoder": "^1.1.1", 1354 | "util-deprecate": "^1.0.1" 1355 | }, 1356 | "engines": { 1357 | "node": ">= 6" 1358 | } 1359 | }, 1360 | "node_modules/readdirp": { 1361 | "version": "3.6.0", 1362 | "license": "MIT", 1363 | "dependencies": { 1364 | "picomatch": "^2.2.1" 1365 | }, 1366 | "engines": { 1367 | "node": ">=8.10.0" 1368 | } 1369 | }, 1370 | "node_modules/retry": { 1371 | "version": "0.12.0", 1372 | "license": "MIT", 1373 | "optional": true, 1374 | "engines": { 1375 | "node": ">= 4" 1376 | } 1377 | }, 1378 | "node_modules/rimraf": { 1379 | "version": "3.0.2", 1380 | "license": "ISC", 1381 | "optional": true, 1382 | "dependencies": { 1383 | "glob": "^7.1.3" 1384 | }, 1385 | "bin": { 1386 | "rimraf": "bin.js" 1387 | }, 1388 | "funding": { 1389 | "url": "https://github.com/sponsors/isaacs" 1390 | } 1391 | }, 1392 | "node_modules/safe-buffer": { 1393 | "version": "5.2.1", 1394 | "funding": [ 1395 | { 1396 | "type": "github", 1397 | "url": "https://github.com/sponsors/feross" 1398 | }, 1399 | { 1400 | "type": "patreon", 1401 | "url": "https://www.patreon.com/feross" 1402 | }, 1403 | { 1404 | "type": "consulting", 1405 | "url": "https://feross.org/support" 1406 | } 1407 | ], 1408 | "license": "MIT" 1409 | }, 1410 | "node_modules/safer-buffer": { 1411 | "version": "2.1.2", 1412 | "license": "MIT", 1413 | "optional": true 1414 | }, 1415 | "node_modules/semver": { 1416 | "version": "7.7.1", 1417 | "license": "ISC", 1418 | "bin": { 1419 | "semver": "bin/semver.js" 1420 | }, 1421 | "engines": { 1422 | "node": ">=10" 1423 | } 1424 | }, 1425 | "node_modules/set-blocking": { 1426 | "version": "2.0.0", 1427 | "license": "ISC", 1428 | "optional": true 1429 | }, 1430 | "node_modules/signal-exit": { 1431 | "version": "3.0.7", 1432 | "license": "ISC", 1433 | "optional": true 1434 | }, 1435 | "node_modules/simple-concat": { 1436 | "version": "1.0.1", 1437 | "funding": [ 1438 | { 1439 | "type": "github", 1440 | "url": "https://github.com/sponsors/feross" 1441 | }, 1442 | { 1443 | "type": "patreon", 1444 | "url": "https://www.patreon.com/feross" 1445 | }, 1446 | { 1447 | "type": "consulting", 1448 | "url": "https://feross.org/support" 1449 | } 1450 | ], 1451 | "license": "MIT" 1452 | }, 1453 | "node_modules/simple-get": { 1454 | "version": "4.0.1", 1455 | "funding": [ 1456 | { 1457 | "type": "github", 1458 | "url": "https://github.com/sponsors/feross" 1459 | }, 1460 | { 1461 | "type": "patreon", 1462 | "url": "https://www.patreon.com/feross" 1463 | }, 1464 | { 1465 | "type": "consulting", 1466 | "url": "https://feross.org/support" 1467 | } 1468 | ], 1469 | "license": "MIT", 1470 | "dependencies": { 1471 | "decompress-response": "^6.0.0", 1472 | "once": "^1.3.1", 1473 | "simple-concat": "^1.0.0" 1474 | } 1475 | }, 1476 | "node_modules/simple-update-notifier": { 1477 | "version": "2.0.0", 1478 | "license": "MIT", 1479 | "dependencies": { 1480 | "semver": "^7.5.3" 1481 | }, 1482 | "engines": { 1483 | "node": ">=10" 1484 | } 1485 | }, 1486 | "node_modules/smart-buffer": { 1487 | "version": "4.2.0", 1488 | "license": "MIT", 1489 | "optional": true, 1490 | "engines": { 1491 | "node": ">= 6.0.0", 1492 | "npm": ">= 3.0.0" 1493 | } 1494 | }, 1495 | "node_modules/socks": { 1496 | "version": "2.8.4", 1497 | "license": "MIT", 1498 | "optional": true, 1499 | "dependencies": { 1500 | "ip-address": "^9.0.5", 1501 | "smart-buffer": "^4.2.0" 1502 | }, 1503 | "engines": { 1504 | "node": ">= 10.0.0", 1505 | "npm": ">= 3.0.0" 1506 | } 1507 | }, 1508 | "node_modules/socks-proxy-agent": { 1509 | "version": "6.2.1", 1510 | "license": "MIT", 1511 | "optional": true, 1512 | "dependencies": { 1513 | "agent-base": "^6.0.2", 1514 | "debug": "^4.3.3", 1515 | "socks": "^2.6.2" 1516 | }, 1517 | "engines": { 1518 | "node": ">= 10" 1519 | } 1520 | }, 1521 | "node_modules/sprintf-js": { 1522 | "version": "1.1.3", 1523 | "license": "BSD-3-Clause", 1524 | "optional": true 1525 | }, 1526 | "node_modules/sqlite": { 1527 | "version": "5.1.1", 1528 | "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz", 1529 | "integrity": "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==" 1530 | }, 1531 | "node_modules/sqlite3": { 1532 | "version": "5.1.7", 1533 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", 1534 | "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", 1535 | "hasInstallScript": true, 1536 | "dependencies": { 1537 | "bindings": "^1.5.0", 1538 | "node-addon-api": "^7.0.0", 1539 | "prebuild-install": "^7.1.1", 1540 | "tar": "^6.1.11" 1541 | }, 1542 | "optionalDependencies": { 1543 | "node-gyp": "8.x" 1544 | }, 1545 | "peerDependencies": { 1546 | "node-gyp": "8.x" 1547 | }, 1548 | "peerDependenciesMeta": { 1549 | "node-gyp": { 1550 | "optional": true 1551 | } 1552 | } 1553 | }, 1554 | "node_modules/ssri": { 1555 | "version": "8.0.1", 1556 | "license": "ISC", 1557 | "optional": true, 1558 | "dependencies": { 1559 | "minipass": "^3.1.1" 1560 | }, 1561 | "engines": { 1562 | "node": ">= 8" 1563 | } 1564 | }, 1565 | "node_modules/string_decoder": { 1566 | "version": "1.3.0", 1567 | "license": "MIT", 1568 | "dependencies": { 1569 | "safe-buffer": "~5.2.0" 1570 | } 1571 | }, 1572 | "node_modules/string-width": { 1573 | "version": "4.2.3", 1574 | "license": "MIT", 1575 | "optional": true, 1576 | "dependencies": { 1577 | "emoji-regex": "^8.0.0", 1578 | "is-fullwidth-code-point": "^3.0.0", 1579 | "strip-ansi": "^6.0.1" 1580 | }, 1581 | "engines": { 1582 | "node": ">=8" 1583 | } 1584 | }, 1585 | "node_modules/strip-ansi": { 1586 | "version": "6.0.1", 1587 | "license": "MIT", 1588 | "optional": true, 1589 | "dependencies": { 1590 | "ansi-regex": "^5.0.1" 1591 | }, 1592 | "engines": { 1593 | "node": ">=8" 1594 | } 1595 | }, 1596 | "node_modules/strip-json-comments": { 1597 | "version": "2.0.1", 1598 | "license": "MIT", 1599 | "engines": { 1600 | "node": ">=0.10.0" 1601 | } 1602 | }, 1603 | "node_modules/supports-color": { 1604 | "version": "5.5.0", 1605 | "license": "MIT", 1606 | "dependencies": { 1607 | "has-flag": "^3.0.0" 1608 | }, 1609 | "engines": { 1610 | "node": ">=4" 1611 | } 1612 | }, 1613 | "node_modules/tar": { 1614 | "version": "6.2.1", 1615 | "license": "ISC", 1616 | "dependencies": { 1617 | "chownr": "^2.0.0", 1618 | "fs-minipass": "^2.0.0", 1619 | "minipass": "^5.0.0", 1620 | "minizlib": "^2.1.1", 1621 | "mkdirp": "^1.0.3", 1622 | "yallist": "^4.0.0" 1623 | }, 1624 | "engines": { 1625 | "node": ">=10" 1626 | } 1627 | }, 1628 | "node_modules/tar-fs": { 1629 | "version": "2.1.3", 1630 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", 1631 | "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", 1632 | "dependencies": { 1633 | "chownr": "^1.1.1", 1634 | "mkdirp-classic": "^0.5.2", 1635 | "pump": "^3.0.0", 1636 | "tar-stream": "^2.1.4" 1637 | } 1638 | }, 1639 | "node_modules/tar-fs/node_modules/chownr": { 1640 | "version": "1.1.4", 1641 | "license": "ISC" 1642 | }, 1643 | "node_modules/tar-stream": { 1644 | "version": "2.2.0", 1645 | "license": "MIT", 1646 | "dependencies": { 1647 | "bl": "^4.0.3", 1648 | "end-of-stream": "^1.4.1", 1649 | "fs-constants": "^1.0.0", 1650 | "inherits": "^2.0.3", 1651 | "readable-stream": "^3.1.1" 1652 | }, 1653 | "engines": { 1654 | "node": ">=6" 1655 | } 1656 | }, 1657 | "node_modules/tar/node_modules/minipass": { 1658 | "version": "5.0.0", 1659 | "license": "ISC", 1660 | "engines": { 1661 | "node": ">=8" 1662 | } 1663 | }, 1664 | "node_modules/technicalindicators": { 1665 | "version": "3.1.0", 1666 | "license": "MIT", 1667 | "dependencies": { 1668 | "@types/node": "^6.0.96" 1669 | } 1670 | }, 1671 | "node_modules/to-regex-range": { 1672 | "version": "5.0.1", 1673 | "license": "MIT", 1674 | "dependencies": { 1675 | "is-number": "^7.0.0" 1676 | }, 1677 | "engines": { 1678 | "node": ">=8.0" 1679 | } 1680 | }, 1681 | "node_modules/touch": { 1682 | "version": "3.1.1", 1683 | "license": "ISC", 1684 | "bin": { 1685 | "nodetouch": "bin/nodetouch.js" 1686 | } 1687 | }, 1688 | "node_modules/tunnel-agent": { 1689 | "version": "0.6.0", 1690 | "license": "Apache-2.0", 1691 | "dependencies": { 1692 | "safe-buffer": "^5.0.1" 1693 | }, 1694 | "engines": { 1695 | "node": "*" 1696 | } 1697 | }, 1698 | "node_modules/tweetnacl": { 1699 | "version": "1.0.3", 1700 | "license": "Unlicense" 1701 | }, 1702 | "node_modules/undefsafe": { 1703 | "version": "2.0.5", 1704 | "license": "MIT" 1705 | }, 1706 | "node_modules/unique-filename": { 1707 | "version": "1.1.1", 1708 | "license": "ISC", 1709 | "optional": true, 1710 | "dependencies": { 1711 | "unique-slug": "^2.0.0" 1712 | } 1713 | }, 1714 | "node_modules/unique-slug": { 1715 | "version": "2.0.2", 1716 | "license": "ISC", 1717 | "optional": true, 1718 | "dependencies": { 1719 | "imurmurhash": "^0.1.4" 1720 | } 1721 | }, 1722 | "node_modules/util-deprecate": { 1723 | "version": "1.0.2", 1724 | "license": "MIT" 1725 | }, 1726 | "node_modules/which": { 1727 | "version": "2.0.2", 1728 | "license": "ISC", 1729 | "optional": true, 1730 | "dependencies": { 1731 | "isexe": "^2.0.0" 1732 | }, 1733 | "bin": { 1734 | "node-which": "bin/node-which" 1735 | }, 1736 | "engines": { 1737 | "node": ">= 8" 1738 | } 1739 | }, 1740 | "node_modules/wide-align": { 1741 | "version": "1.1.5", 1742 | "license": "ISC", 1743 | "optional": true, 1744 | "dependencies": { 1745 | "string-width": "^1.0.2 || 2 || 3 || 4" 1746 | } 1747 | }, 1748 | "node_modules/wrappy": { 1749 | "version": "1.0.2", 1750 | "license": "ISC" 1751 | }, 1752 | "node_modules/ws": { 1753 | "version": "8.18.1", 1754 | "license": "MIT", 1755 | "engines": { 1756 | "node": ">=10.0.0" 1757 | }, 1758 | "peerDependencies": { 1759 | "bufferutil": "^4.0.1", 1760 | "utf-8-validate": ">=5.0.2" 1761 | }, 1762 | "peerDependenciesMeta": { 1763 | "bufferutil": { 1764 | "optional": true 1765 | }, 1766 | "utf-8-validate": { 1767 | "optional": true 1768 | } 1769 | } 1770 | }, 1771 | "node_modules/yallist": { 1772 | "version": "4.0.0", 1773 | "license": "ISC" 1774 | } 1775 | } 1776 | } 1777 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon app.js", 9 | "prod": "node app.js" 10 | }, 11 | "keywords": [], 12 | "author": "@heron_jr", 13 | "license": "MIT", 14 | "type": "module", 15 | "dependencies": { 16 | "axios": "^1.8.4", 17 | "bs58": "^6.0.0", 18 | "cron": "^4.3.0", 19 | "dotenv": "^16.5.0", 20 | "nodemon": "^3.1.9", 21 | "openai": "^5.3.0", 22 | "sqlite": "^5.1.1", 23 | "sqlite3": "^5.1.7", 24 | "technicalindicators": "^3.1.0", 25 | "tweetnacl": "^1.0.3", 26 | "ws": "^8.18.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Achievements/Achievements.js: -------------------------------------------------------------------------------- 1 | import Futures from '../Backpack/Authenticated/Futures.js'; 2 | import OrderController from '../Controllers/OrderController.js'; 3 | import AccountController from '../Controllers/AccountController.js' 4 | import Order from '../Backpack/Authenticated/Order.js'; 5 | import PnlController from '../Controllers/PnlController.js'; 6 | import Markets from '../Backpack/Public/Markets.js' 7 | import Account from '../Backpack/Authenticated/Account.js'; 8 | import Capital from '../Backpack/Authenticated/Capital.js'; 9 | 10 | class Achievements { 11 | 12 | check24Hour(date) { 13 | const inputDate = new Date(date) 14 | const now = new Date() 15 | const diffInMs = now - inputDate 16 | const hoursDiff = diffInMs / (1000 * 60 * 60) 17 | return hoursDiff < 24 18 | } 19 | 20 | 21 | async checkVolume() { 22 | try { 23 | const SPOT_MARKET = String(process.env.SPOT_MARKET) 24 | const START_UTC = String(process.env.START_UTC) 25 | const GOAL_VOLUME = Number(process.env.GOAL_VOLUME) 26 | const results = await PnlController.getVolumeMarket(SPOT_MARKET, START_UTC) 27 | 28 | const limitTime = this.check24Hour(START_UTC) 29 | 30 | if(!limitTime) { 31 | console.log("Passed the first 24 hours ☠️") 32 | } 33 | 34 | if(results) { 35 | 36 | let {totalVolume, totalFee} = results 37 | 38 | totalVolume = Number(totalVolume.toFixed(0)) 39 | totalFee = Number(totalFee.toFixed(2)) 40 | 41 | console.log("") 42 | 43 | console.log("💸 Fees", totalFee) 44 | console.log("📈 Current Volume", totalVolume) 45 | console.log("🎯 Goal", GOAL_VOLUME) 46 | 47 | if(totalVolume >= GOAL_VOLUME) { 48 | console.log("🎉 Congratulations, you reached your goal!", totalVolume) 49 | } else { 50 | console.log("📈 Remaining Volume",GOAL_VOLUME - totalVolume) 51 | } 52 | 53 | console.log("") 54 | 55 | if(!limitTime) { 56 | return null 57 | } 58 | 59 | return (totalVolume - GOAL_VOLUME) 60 | 61 | } else { 62 | return null 63 | } 64 | 65 | } catch (error) { 66 | console.log(error) 67 | return null 68 | } 69 | } 70 | 71 | async run() { 72 | try { 73 | 74 | const {volume} = await this.checkVolume() 75 | 76 | if(volume) { 77 | const SPOT_MARKET = String(process.env.SPOT_MARKET) 78 | const GOAL_VOLUME = Number(process.env.GOAL_VOLUME) 79 | 80 | const markets = await Markets.getMarkets() 81 | const market = markets.find((el) => el.symbol === SPOT_MARKET) 82 | 83 | const stepSize_quantity = Number(market.filters.quantity.stepSize) 84 | const decimal_quantity = String(market.filters.quantity.stepSize).includes(".") ? String(market.filters.quantity.stepSize).split(".")[1].length : 0; 85 | 86 | const Balances = await Capital.getBalances() 87 | const Collateral = await Capital.getCollateral() 88 | 89 | const amout = Number(Balances[market.baseSymbol].available) 90 | const usdc = String(Number(Collateral.collateral.find((el) => el.symbol === "USDC").collateralValue).toFixed(0)) 91 | 92 | const formatQuantity = (value) => parseFloat(value).toFixed(decimal_quantity).toString(); 93 | 94 | if(amout < 1) { 95 | await OrderController.openOrderSpot({side: "Bid", symbol : SPOT_MARKET, volume: usdc, quantity:usdc}) 96 | console.log("Comprando", usdc, "dolares em",market.baseSymbol) 97 | } else { 98 | console.log("Vendendo", amout, "de ",market.baseSymbol, "em USDC") 99 | const quantity = formatQuantity(Math.floor(amout / stepSize_quantity) * stepSize_quantity); 100 | await OrderController.openOrderSpot({side: "Ask", symbol : SPOT_MARKET, volume: usdc, quantity}) 101 | } 102 | 103 | await new Promise(resolve => setTimeout(resolve, 5000)); 104 | 105 | await this.run() 106 | } 107 | 108 | } catch (error) { 109 | console.log(error) 110 | return false 111 | } 112 | } 113 | 114 | } 115 | 116 | export default new Achievements(); 117 | 118 | -------------------------------------------------------------------------------- /src/Backpack/Authenticated/Account.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { auth } from './Authentication.js'; 3 | 4 | class Account { 5 | 6 | async getAccount() { 7 | const timestamp = Date.now(); 8 | 9 | const headers = auth({ 10 | instruction: 'accountQuery', 11 | timestamp, 12 | params: {}, 13 | }); 14 | 15 | try { 16 | const response = await axios.get(`${process.env.API_URL}/api/v1/account`, { 17 | headers, 18 | }); 19 | 20 | return response.data; 21 | } catch (error) { 22 | console.error('getAccount - ERROR!', error.response?.data || error.message); 23 | return null; 24 | } 25 | } 26 | 27 | // Symbol is token symbol not market, ex: BTC, SOL, etc. 28 | async getMaxBorrowQuantity(symbol) { 29 | const timestamp = Date.now(); 30 | 31 | if (!symbol) { 32 | console.error('symbol required'); 33 | return null; 34 | } 35 | 36 | const headers = auth({ 37 | instruction: 'maxBorrowQuantity', 38 | timestamp, 39 | params: { symbol }, 40 | }); 41 | 42 | try { 43 | const response = await axios.get(`${process.env.API_URL}/api/v1/account/limits/borrow`, { 44 | headers, 45 | params: { symbol }, 46 | }); 47 | 48 | return response.data; 49 | } catch (error) { 50 | console.error('getMaxBorrowQuantity - ERROR!', error.response?.data || error.message); 51 | return null; 52 | } 53 | } 54 | 55 | //side: "Bid" "Ask" 56 | async getMaxOrderQuantity(symbol, side) { 57 | const timestamp = Date.now(); 58 | 59 | if (!symbol) { 60 | console.error('symbol required'); 61 | return null; 62 | } 63 | 64 | if (!side) { 65 | console.error('side required'); 66 | return null; 67 | } 68 | 69 | const headers = auth({ 70 | instruction: 'maxOrderQuantity', 71 | timestamp, 72 | params: {symbol, side}, 73 | }); 74 | 75 | try { 76 | const response = await axios.get(`${process.env.API_URL}/api/v1/account/limits/order`, { 77 | headers, 78 | params: {symbol, side}, 79 | }); 80 | 81 | return response.data; 82 | } catch (error) { 83 | console.error('getMaxOrderQuantity - ERROR!', error.response?.data || error.message); 84 | return null; 85 | } 86 | } 87 | 88 | async getMaxWithdrawalQuantity(symbol, autoBorrow = true, autoLendRedeem = true) { 89 | const timestamp = Date.now(); 90 | 91 | if (!symbol) { 92 | console.error('symbol required'); 93 | return null; 94 | } 95 | 96 | const headers = auth({ 97 | instruction: 'maxWithdrawalQuantity', 98 | timestamp, 99 | params: {symbol, autoBorrow, autoLendRedeem}, 100 | }); 101 | 102 | try { 103 | const response = await axios.get(`${process.env.API_URL}/api/v1/account/limits/withdrawal`, { 104 | headers, 105 | params: {symbol, autoBorrow, autoLendRedeem} 106 | }); 107 | return response.data; 108 | } catch (error) { 109 | console.error('getMaxWithdrawalQuantity - ERROR!', error.response?.data || error.message); 110 | return null; 111 | } 112 | } 113 | 114 | async updateAccount(leverageLimit, 115 | autoBorrowSettlements = true, 116 | autoLend = true, 117 | autoRepayBorrows = true, 118 | ) { 119 | const timestamp = Date.now(); 120 | 121 | if (!leverageLimit) { 122 | console.error('symbol required'); 123 | return null; 124 | } 125 | 126 | const params = { 127 | autoBorrowSettlements, 128 | autoLend, 129 | autoRepayBorrows, 130 | leverageLimit 131 | }; 132 | 133 | const headers = auth({ 134 | instruction: 'accountUpdate', 135 | timestamp, 136 | params, 137 | }); 138 | 139 | try { 140 | const response = await axios.patch(`${process.env.API_URL}/api/v1/account`, params, { 141 | headers, 142 | }); 143 | return response.data; 144 | } catch (error) { 145 | console.error('updateAccount - ERROR!', error.response?.data || error.message); 146 | return null; 147 | } 148 | } 149 | 150 | } 151 | 152 | export default new Account(); 153 | -------------------------------------------------------------------------------- /src/Backpack/Authenticated/Authentication.js: -------------------------------------------------------------------------------- 1 | import nacl from 'tweetnacl'; 2 | 3 | export function auth({ instruction, params = {}, timestamp, window = 10000 }) { 4 | const privateKeySeed = Buffer.from(process.env.BACKPACK_API_SECRET, 'base64'); 5 | const keyPair = nacl.sign.keyPair.fromSeed(privateKeySeed); 6 | 7 | const sortedParams = Object.keys(params) 8 | .sort() 9 | .map(key => `${key}=${params[key]}`) 10 | .join('&'); 11 | 12 | const baseString = sortedParams ? `${sortedParams}&` : ''; 13 | const payload = `instruction=${instruction}&${baseString}timestamp=${timestamp}&window=${window}`; 14 | 15 | const signature = nacl.sign.detached(Buffer.from(payload), keyPair.secretKey); 16 | 17 | return { 18 | 'X-API-Key': process.env.BACKPACK_API_KEY, 19 | 'X-Signature': Buffer.from(signature).toString('base64'), 20 | 'X-Timestamp': timestamp.toString(), 21 | 'X-Window': window.toString(), 22 | 'Content-Type' : 'application/json; charset=utf-8' 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/Backpack/Authenticated/BorrowLend.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { auth } from './Authentication.js'; 3 | 4 | class BorrowLend { 5 | 6 | async getBorrowLendPositionQuery() { 7 | const timestamp = Date.now(); 8 | 9 | const headers = auth({ 10 | instruction: 'borrowLendPositionQuery', 11 | timestamp, 12 | }); 13 | 14 | try { 15 | const response = await axios.get(`${process.env.API_URL}/api/v1/borrowLend/positions`, { 16 | headers, 17 | }); 18 | 19 | return response.data; 20 | } catch (error) { 21 | console.error('getBorrowLendPositionQuery - ERROR!', error.response?.data || error.message); 22 | return null; 23 | } 24 | } 25 | 26 | async borrowLendExecute(symbol, side, quantity) { 27 | const timestamp = Date.now(); 28 | 29 | if (!symbol) { 30 | console.error('symbol required'); 31 | return null; 32 | } 33 | 34 | if (!side) { 35 | console.error('side required'); 36 | return null; 37 | } 38 | 39 | if (!quantity) { 40 | console.error('quantity required'); 41 | return null; 42 | } 43 | 44 | const body = { 45 | symbol, //symbol token "BTC" "ETH" "SOL" 46 | side, // "Borrow" ou "Lend" 47 | quantity, // string, exemplo: "0.01" 48 | }; 49 | 50 | const headers = auth({ 51 | instruction: 'borrowLendExecute', 52 | timestamp, 53 | params: body, 54 | }); 55 | 56 | try { 57 | const response = await axios.post(`${process.env.API_URL}/api/v1/borrowLend`, body, { 58 | headers, 59 | }); 60 | 61 | return response.data; 62 | } catch (error) { 63 | console.error('borrowLendExecute - ERROR!', error.response?.data || error.message); 64 | return null; 65 | } 66 | } 67 | 68 | } 69 | 70 | export default new BorrowLend(); 71 | -------------------------------------------------------------------------------- /src/Backpack/Authenticated/Capital.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { auth } from './Authentication.js'; 3 | 4 | class Capital { 5 | 6 | async getBalances() { 7 | const timestamp = Date.now(); 8 | 9 | const headers = auth({ 10 | instruction: 'balanceQuery', 11 | timestamp, 12 | }); 13 | 14 | try { 15 | const response = await axios.get(`${process.env.API_URL}/api/v1/capital`, { 16 | headers, 17 | }); 18 | 19 | return response.data 20 | } catch (error) { 21 | console.error('getBalances - ERROR!', error.response?.data || error.message); 22 | return null 23 | } 24 | } 25 | 26 | async getCollateral() { 27 | const timestamp = Date.now(); 28 | 29 | const headers = auth({ 30 | instruction: 'collateralQuery', 31 | timestamp, 32 | params: {}, // Sem parâmetros nesse caso 33 | }); 34 | 35 | try { 36 | const response = await axios.get(`${process.env.API_URL}/api/v1/capital/collateral`, { 37 | headers, 38 | }); 39 | 40 | return response.data 41 | } catch (error) { 42 | console.error('getCollateral - ERROR!', error.response?.data || error.message); 43 | return null 44 | } 45 | } 46 | 47 | async getDeposits( 48 | from = Date.now() - 30 * 24 * 60 * 60 * 1000, // 30 days 49 | to = Date.now(), // now 50 | limit = 100, 51 | offset = 0 52 | ) { 53 | const timestamp = Date.now(); 54 | 55 | const params = { from, to, limit, offset }; 56 | 57 | const headers = auth({ 58 | instruction: 'depositQueryAll', 59 | timestamp, 60 | params 61 | }); 62 | 63 | try { 64 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/capital/deposits`, { 65 | headers, 66 | params 67 | }); 68 | 69 | return response.data; 70 | } catch (error) { 71 | console.error('getDeposits - ERROR!', error.response?.data || error.message); 72 | return null; 73 | } 74 | } 75 | 76 | // blockchain: "Arbitrum" "Base" "Berachain" "Bitcoin" "BitcoinCash" "Bsc" "Cardano" "Dogecoin" "EqualsMoney" "Ethereum" "Hyperliquid" "Litecoin" "Polygon" "Sui" "Solana" "Story" "XRP" 77 | async getDepositAddress(blockchain) { 78 | const timestamp = Date.now(); 79 | 80 | if (!blockchain) { 81 | console.error('blockchain required'); 82 | return null; 83 | } 84 | 85 | const params = {blockchain} 86 | 87 | const headers = auth({ 88 | instruction: 'depositAddressQuery', 89 | timestamp, 90 | params, 91 | }); 92 | 93 | try { 94 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/capital/deposit/address`, { 95 | headers, 96 | params: params 97 | }); 98 | 99 | return response.data 100 | } catch (error) { 101 | console.error('getDepositAddress - ERROR!', error.response?.data || error.message); 102 | return null 103 | } 104 | } 105 | 106 | async getWithdrawals( 107 | from = Date.now() - 30 * 24 * 60 * 60 * 1000, // 30 days 108 | to = Date.now(), // agora 109 | limit = 100, 110 | offset = 0 111 | ) { 112 | const timestamp = Date.now(); 113 | 114 | const params = { from, to, limit, offset }; 115 | 116 | const headers = auth({ 117 | instruction: 'withdrawalQueryAll', 118 | timestamp, 119 | params 120 | }); 121 | 122 | try { 123 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/capital/withdrawals`, { 124 | headers, 125 | params 126 | }); 127 | 128 | return response.data; 129 | } catch (error) { 130 | console.error('getWithdrawals - ERROR!', error.response?.data || error.message); 131 | return null; 132 | } 133 | } 134 | 135 | } 136 | 137 | export default new Capital(); 138 | -------------------------------------------------------------------------------- /src/Backpack/Authenticated/Futures.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { auth } from './Authentication.js'; 3 | 4 | class Futures { 5 | 6 | async getOpenPositions() { 7 | const timestamp = Date.now(); 8 | const headers = auth({ 9 | instruction: 'positionQuery', 10 | timestamp, 11 | }); 12 | try { 13 | const response = await axios.get(`${process.env.API_URL}/api/v1/position`, { 14 | headers, 15 | }); 16 | return response.data 17 | } catch (error) { 18 | console.error('getOpenPositions - ERROR!', error.response?.data || error.message); 19 | return null 20 | } 21 | } 22 | 23 | } 24 | 25 | export default new Futures(); 26 | -------------------------------------------------------------------------------- /src/Backpack/Authenticated/History.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { auth } from './Authentication.js'; 3 | 4 | class History { 5 | 6 | async getBorrowHistory(symbol, type, limit, offset, sortDirection, positionId, sources) { 7 | const timestamp = Date.now(); 8 | 9 | const params = {}; 10 | if (symbol) params.symbol = symbol; 11 | if (type) params.type = type; 12 | if (limit) params.limit = limit; 13 | if (offset) params.offset = offset; 14 | if (sortDirection) params.sortDirection = sortDirection; 15 | if (positionId) params.positionId = positionId; 16 | if (sources) params.sources = sources; 17 | 18 | const headers = auth({ 19 | instruction: 'borrowHistoryQueryAll', 20 | timestamp, 21 | params: params, 22 | }); 23 | 24 | try { 25 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/history/borrowLend`, { 26 | headers, 27 | params 28 | }); 29 | 30 | return response.data 31 | } catch (error) { 32 | console.error('getBorrowHistory - ERROR!', error.response?.data || error.message); 33 | return null 34 | } 35 | } 36 | 37 | async getInterestHistory(symbol, type, limit, offset, sortDirection, positionId, sources) { 38 | const timestamp = Date.now(); 39 | 40 | const params = {}; 41 | if (symbol) params.symbol = symbol; 42 | if (type) params.type = type; 43 | if (limit) params.limit = limit; 44 | if (offset) params.offset = offset; 45 | if (sortDirection) params.sortDirection = sortDirection; 46 | if (positionId) params.positionId = positionId; 47 | if (sources) params.sources = sources; 48 | 49 | const headers = auth({ 50 | instruction: 'interestHistoryQueryAll', 51 | timestamp, 52 | params: params, 53 | }); 54 | 55 | try { 56 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/history/interest`, { 57 | headers, 58 | params 59 | }); 60 | 61 | return response.data 62 | } catch (error) { 63 | console.error('getInterestHistory - ERROR!', error.response?.data || error.message); 64 | return null 65 | } 66 | } 67 | 68 | async getBorrowPositionHistory(symbol, side, state, limit, offset, sortDirection) { 69 | const timestamp = Date.now(); 70 | 71 | const params = {}; 72 | if (symbol) params.symbol = symbol; 73 | if (side) params.type = type; 74 | if (state) params.state = state; 75 | if (limit) params.limit = limit; 76 | if (offset) params.offset = offset; 77 | if (sortDirection) params.sortDirection = sortDirection; 78 | 79 | const headers = auth({ 80 | instruction: 'borrowPositionHistoryQueryAll', 81 | timestamp, 82 | params: params, 83 | }); 84 | 85 | try { 86 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/history/borrowLend/positions`, { 87 | headers, 88 | params 89 | }); 90 | 91 | return response.data 92 | } catch (error) { 93 | console.error('getBorrowPositionHistory - ERROR!', error.response?.data || error.message); 94 | return null 95 | } 96 | } 97 | 98 | async getFillHistory(symbol, orderId, from, to, limit, offset, fillType, marketType, sortDirection) { 99 | const timestamp = Date.now(); 100 | 101 | const params = {}; 102 | if (orderId) params.orderId = orderId; 103 | if (from) params.from = from; 104 | if (to) params.to = to; 105 | if (symbol) params.symbol = symbol; 106 | if (limit) params.limit = limit; 107 | if (offset) params.offset = offset; 108 | if (fillType) params.fillType = fillType; 109 | if (marketType) params.marketType = marketType; // array if multi values 110 | if (sortDirection) params.sortDirection = sortDirection; 111 | 112 | const headers = auth({ 113 | instruction: 'fillHistoryQueryAll', 114 | timestamp, 115 | params, 116 | }); 117 | 118 | try { 119 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/history/fills`, { 120 | headers, 121 | params, 122 | }); 123 | 124 | return response.data; 125 | } catch (error) { 126 | console.error('getFillHistory - ERROR!', error.response?.data || error.message); 127 | return null; 128 | } 129 | } 130 | 131 | async getFundingPayments(symbol, limit, offset, sortDirection) { 132 | const timestamp = Date.now(); 133 | 134 | const params = {}; 135 | if (symbol) params.symbol = symbol; 136 | if (limit) params.limit = limit; 137 | if (offset) params.offset = offset; 138 | if (sortDirection) params.sortDirection = sortDirection; 139 | 140 | const headers = auth({ 141 | instruction: 'fundingHistoryQueryAll', 142 | timestamp, 143 | params, 144 | }); 145 | 146 | try { 147 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/history/funding`, { 148 | headers, 149 | params, 150 | }); 151 | 152 | return response.data; 153 | } catch (error) { 154 | console.error('getFundingPayments - ERROR!', error.response?.data || error.message); 155 | return null; 156 | } 157 | } 158 | 159 | async getOrderHistory(orderId, symbol, limit, offset, marketType, sortDirection) { 160 | const timestamp = Date.now(); 161 | 162 | const params = {}; 163 | if (orderId) params.orderId = orderId; 164 | if (symbol) params.symbol = symbol; 165 | if (limit) params.limit = limit; 166 | if (offset) params.offset = offset; 167 | if (marketType) params.marketType = marketType; 168 | if (sortDirection) params.sortDirection = sortDirection; 169 | 170 | const headers = auth({ 171 | instruction: 'orderHistoryQueryAll', 172 | timestamp, 173 | params, 174 | }); 175 | 176 | try { 177 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/history/orders`, { 178 | headers, 179 | params, 180 | }); 181 | 182 | return response.data; 183 | } catch (error) { 184 | console.error('getOrderHistory - ERROR!', error.response?.data || error.message); 185 | return null; 186 | } 187 | } 188 | 189 | async getProfitAndLossHistory(subaccountId, symbol, limit, offset, sortDirection) { 190 | const timestamp = Date.now(); 191 | 192 | const params = {}; 193 | if (subaccountId) params.subaccountId = subaccountId; 194 | if (symbol) params.symbol = symbol; 195 | if (limit) params.limit = limit; 196 | if (offset) params.offset = offset; 197 | if (sortDirection) params.sortDirection = sortDirection; 198 | 199 | const headers = auth({ 200 | instruction: 'pnlHistoryQueryAll', 201 | timestamp, 202 | params, 203 | }); 204 | 205 | try { 206 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/history/pnl`, { 207 | headers, 208 | params, 209 | }); 210 | 211 | return response.data; 212 | } catch (error) { 213 | console.error('getProfitAndLossHistory - ERROR!', error.response?.data || error.message); 214 | return null; 215 | } 216 | } 217 | 218 | //source: "BackstopLiquidation" "CulledBorrowInterest" "CulledRealizePnl" "CulledRealizePnlBookUtilization" "FundingPayment" "RealizePnl" "TradingFees" "TradingFeesSystem" 219 | async getSettlementHistory(limit, offset, source, sortDirection) { 220 | const timestamp = Date.now(); 221 | 222 | const params = {}; 223 | if (limit) params.limit = limit; 224 | if (offset) params.offset = offset; 225 | if (source) params.source = source; 226 | if (sortDirection) params.sortDirection = sortDirection; 227 | 228 | const headers = auth({ 229 | instruction: 'settlementHistoryQueryAll', 230 | timestamp, 231 | params, 232 | }); 233 | 234 | try { 235 | const response = await axios.get(`${process.env.API_URL}/wapi/v1/history/settlement`, { 236 | headers, 237 | params, 238 | }); 239 | 240 | return response.data; 241 | } catch (error) { 242 | console.error('getSettlementHistory - ERROR!', error.response?.data || error.message); 243 | return null; 244 | } 245 | } 246 | 247 | } 248 | 249 | export default new History(); 250 | -------------------------------------------------------------------------------- /src/Backpack/Authenticated/Order.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { auth } from './Authentication.js'; 3 | 4 | class Order { 5 | 6 | async getOpenOrder(symbol, orderId, clientId) { 7 | const timestamp = Date.now(); 8 | 9 | if (!symbol) { 10 | console.error('symbol required'); 11 | return null; 12 | } 13 | 14 | if (!orderId && !clientId) { 15 | console.error('clientId or orderId is required'); 16 | return null; 17 | } 18 | 19 | 20 | const params = {} 21 | if (symbol) params.symbol = symbol; 22 | if (orderId) params.orderId = orderId; 23 | if (clientId) params.clientId = clientId; 24 | 25 | const headers = auth({ 26 | instruction: 'orderQuery', 27 | timestamp, 28 | params 29 | }); 30 | 31 | try { 32 | const response = await axios.get(`${process.env.API_URL}/api/v1/order`, { 33 | headers, 34 | params 35 | }); 36 | return response.data 37 | } catch (error) { 38 | console.error('getOpenOrder - ERROR!', error.response?.data || error.message); 39 | return null 40 | } 41 | } 42 | 43 | //marketType: "SPOT" "PERP" "IPERP" "DATED" "PREDICTION" "RFQ" 44 | async getOpenOrders(symbol, marketType = "PERP") { 45 | const timestamp = Date.now(); 46 | 47 | const params = {} 48 | if (symbol) params.symbol = symbol; 49 | if (marketType) params.marketType = marketType; 50 | 51 | const headers = auth({ 52 | instruction: 'orderQueryAll', 53 | timestamp, 54 | params 55 | }); 56 | 57 | try { 58 | const response = await axios.get(`${process.env.API_URL}/api/v1/orders`, { 59 | headers, 60 | params 61 | }); 62 | return response.data 63 | } catch (error) { 64 | console.error('getOpenOrders - ERROR!', error.response?.data || error.message); 65 | return null 66 | } 67 | } 68 | 69 | /* 70 | { 71 | "autoLend": true, 72 | "autoLendRedeem": true, 73 | "autoBorrow": true, 74 | "autoBorrowRepay": true, 75 | "clientId": 0, 76 | "orderType": "Market", 77 | "postOnly": true, 78 | "price": "string", 79 | "quantity": "string", 80 | "quoteQuantity": "string", 81 | "reduceOnly": true, 82 | "selfTradePrevention": "RejectTaker", 83 | "side": "Bid", 84 | "stopLossLimitPrice": "string", 85 | "stopLossTriggerBy": "string", 86 | "stopLossTriggerPrice": "string", 87 | "symbol": "string", 88 | "takeProfitLimitPrice": "string", 89 | "takeProfitTriggerBy": "string", 90 | "takeProfitTriggerPrice": "string", 91 | "timeInForce": "GTC", 92 | "triggerBy": "string", 93 | "triggerPrice": "string", 94 | "triggerQuantity": "string" 95 | } 96 | */ 97 | 98 | async executeOrder(body) { 99 | 100 | const timestamp = Date.now(); 101 | const headers = auth({ 102 | instruction: 'orderExecute', 103 | timestamp, 104 | params: body 105 | }); 106 | 107 | try { 108 | const { data } = await axios.post(`${process.env.API_URL}/api/v1/order`, body, { 109 | headers 110 | }); 111 | console.log('✅ executeOrder Success!', data.symbol); 112 | return data; 113 | } catch (error) { 114 | console.error('❌ executeOrder - Error!', error.response?.data || error.message); 115 | return null; 116 | } 117 | } 118 | 119 | 120 | async cancelOpenOrder(symbol, orderId, clientId) { 121 | const timestamp = Date.now(); 122 | 123 | if (!symbol) { 124 | console.error('symbol required'); 125 | return null; 126 | } 127 | 128 | const params = {} 129 | if (symbol) params.symbol = symbol; 130 | if (orderId) params.orderId = orderId; 131 | if (clientId) params.clientId = clientId; 132 | 133 | const headers = auth({ 134 | instruction: 'orderCancel', 135 | timestamp, 136 | params: params 137 | }); 138 | 139 | try { 140 | const response = await axios.delete(`${process.env.API_URL}/api/v1/order`, { 141 | headers, 142 | data:params 143 | }); 144 | return response.data 145 | } catch (error) { 146 | console.error('cancelOpenOrder - ERROR!', error.response?.data || error.message); 147 | return null 148 | } 149 | 150 | } 151 | 152 | async cancelOpenOrders(symbol, orderType) { 153 | const timestamp = Date.now(); 154 | 155 | if (!symbol) { 156 | console.error('symbol required'); 157 | return null; 158 | } 159 | 160 | const params = {} 161 | if (symbol) params.symbol = symbol; 162 | if (orderType) params.orderType = orderType; 163 | 164 | const headers = auth({ 165 | instruction: 'orderCancelAll', 166 | timestamp, 167 | params: params // isso é fundamental para assinatura correta 168 | }); 169 | 170 | try { 171 | const response = await axios.delete(`${process.env.API_URL}/api/v1/orders`, { 172 | headers, 173 | data:params 174 | }); 175 | return response.data 176 | } catch (error) { 177 | console.error('cancelOpenOrders - ERROR!', error.response?.data || error.message); 178 | return null 179 | } 180 | } 181 | 182 | } 183 | 184 | export default new Order(); 185 | -------------------------------------------------------------------------------- /src/Backpack/Public/Assets.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | class Assets { 4 | 5 | async getAssets() { 6 | try { 7 | const response = await axios.get(`${process.env.API_URL}/api/v1/assets`); 8 | return response.data 9 | } catch (error) { 10 | console.error('getAssets - ERROR!', error.response?.data || error.message); 11 | return null; 12 | } 13 | } 14 | 15 | 16 | async getCollateral() { 17 | try { 18 | const response = await axios.get(`${process.env.API_URL}/api/v1/collateral`); 19 | return response.data 20 | } catch (error) { 21 | console.error('getCollateral - ERROR!', error.response?.data || error.message); 22 | return null; 23 | } 24 | } 25 | 26 | 27 | } 28 | 29 | export default new Assets(); 30 | -------------------------------------------------------------------------------- /src/Backpack/Public/BorrowLend.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | class BorrowLend { 4 | 5 | async getMarkets() { 6 | try { 7 | const response = await axios.get(`${process.env.API_URL}/api/v1/borrowLend/markets`); 8 | return response.data 9 | } catch (error) { 10 | console.error('getMarkets - ERROR!', error.response?.data || error.message); 11 | return null; 12 | } 13 | } 14 | 15 | async getHistory(symbol, interval = "1d") { 16 | 17 | if(!symbol){ 18 | console.error('symbol required'); 19 | return null 20 | } 21 | 22 | try { 23 | const response = await axios.get(`${process.env.API_URL}/api/v1/borrowLend/markets/history`, { 24 | params:{symbol, interval}, 25 | }); 26 | return response.data 27 | } catch (error) { 28 | console.error('getHistory - ERROR!', error.response?.data || error.message); 29 | return null; 30 | } 31 | } 32 | 33 | } 34 | 35 | export default new BorrowLend(); 36 | -------------------------------------------------------------------------------- /src/Backpack/Public/Markets.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Utils from '../../utils/Utils.js'; 3 | 4 | class Markets { 5 | 6 | async getMarkets() { 7 | try { 8 | const response = await axios.get(`${process.env.API_URL}/api/v1/markets`); 9 | return response.data 10 | } catch (error) { 11 | console.error('getMarkets - ERROR!', error.response?.data || error.message); 12 | return null; 13 | } 14 | } 15 | 16 | async getMarket(symbol) { 17 | 18 | if(!symbol){ 19 | console.error('symbol required'); 20 | return null 21 | } 22 | 23 | try { 24 | const response = await axios.get(`${process.env.API_URL}/api/v1/market`, { 25 | params:{symbol}, 26 | }) 27 | return response.data 28 | } catch (error) { 29 | console.error('getMarket - ERROR!', error.response?.data || error.message); 30 | return null; 31 | } 32 | } 33 | 34 | async getTickers(interval = "1d") { 35 | try { 36 | 37 | const response = await axios.get(`${process.env.API_URL}/api/v1/tickers`, { 38 | params:{interval}, 39 | }) 40 | 41 | return response.data 42 | } catch (error) { 43 | console.error('getTickers - ERROR!', error.response?.data || error.message); 44 | return null; 45 | } 46 | } 47 | 48 | async getTicker(symbol, interval = "1d") { 49 | 50 | if(!symbol){ 51 | console.error('symbol required'); 52 | return null 53 | } 54 | 55 | try { 56 | 57 | const response = await axios.get(`${process.env.API_URL}/api/v1/ticker`, { 58 | params:{symbol, interval}, 59 | }) 60 | 61 | return response.data 62 | } catch (error) { 63 | console.error('getTicker - ERROR!', error.response?.data || error.message); 64 | return null; 65 | } 66 | } 67 | 68 | async getDepth(symbol) { 69 | 70 | if(!symbol){ 71 | console.error('symbol required'); 72 | return null 73 | } 74 | 75 | try { 76 | const response = await axios.get(`${process.env.API_URL}/api/v1/depth`, { 77 | params:{symbol}, 78 | }) 79 | return response.data 80 | } catch (error) { 81 | console.error('getDepth - ERROR!', error.response?.data || error.message); 82 | return null; 83 | } 84 | } 85 | 86 | 87 | 88 | 89 | async getKLines(symbol, interval, limit) { 90 | 91 | if(!symbol){ 92 | console.error('symbol required'); 93 | return null 94 | } 95 | 96 | if(!interval){ 97 | console.error('interval required'); 98 | return null 99 | } 100 | 101 | if(!limit){ 102 | console.error('limit required'); 103 | return null 104 | } 105 | 106 | try { 107 | const timestamp = Date.now(); 108 | const now = Math.floor(timestamp / 1000); 109 | const duration = Utils.getIntervalInSeconds(interval) * limit; 110 | const startTime = now - duration; 111 | const endTime = now; 112 | 113 | const url = `${process.env.API_URL}/api/v1/klines`; 114 | 115 | const response = await axios.get(url, { 116 | params: { 117 | symbol, 118 | interval, 119 | startTime, 120 | endTime 121 | } 122 | }); 123 | 124 | const data = response.data; 125 | return data 126 | } catch (error) { 127 | console.error('getKLines - ERROR!', error.response?.data || error.message); 128 | return null; 129 | } 130 | } 131 | 132 | async getAllMarkPrices(symbol) { 133 | try { 134 | const response = await axios.get(`${process.env.API_URL}/api/v1/markPrices`, { 135 | params:{symbol}, 136 | }) 137 | return response.data 138 | } catch (error) { 139 | console.error('getAllMarkPrices - ERROR!', error.response?.data || error.message); 140 | return null; 141 | } 142 | } 143 | 144 | async getOpenInterest(symbol) { 145 | try { 146 | const response = await axios.get(`${process.env.API_URL}/api/v1/openInterest`, { 147 | params:{symbol}, 148 | }) 149 | return response.data 150 | } catch (error) { 151 | console.error('getOpenInterest - ERROR!', error.response?.data || error.message); 152 | return null; 153 | } 154 | } 155 | 156 | async getFundingIntervalRates(symbol, limit = 100, offset = 0) { 157 | 158 | if(!symbol){ 159 | console.error('symbol required'); 160 | return null 161 | } 162 | 163 | try { 164 | const response = await axios.get(`${process.env.API_URL}/api/v1/fundingRates`, { 165 | params:{symbol, limit, offset}, 166 | }) 167 | return response.data 168 | } catch (error) { 169 | console.error('getFundingIntervalRates - ERROR!', error.response?.data || error.message); 170 | return null; 171 | } 172 | } 173 | 174 | } 175 | 176 | export default new Markets(); 177 | -------------------------------------------------------------------------------- /src/Backpack/Public/System.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | class System { 4 | 5 | async getStatus() { 6 | 7 | try { 8 | const response = await axios.get(`${process.env.API_URL}/api/v1/status`); 9 | return response.data 10 | } catch (error) { 11 | console.error('getStatus - ERROR!', error.response?.data || error.message); 12 | return null; 13 | } 14 | 15 | } 16 | 17 | async getPing() { 18 | 19 | try { 20 | const response = await axios.get(`${process.env.API_URL}/api/v1/ping`); 21 | return response.data 22 | } catch (error) { 23 | console.error('getPing - ERROR!', error.response?.data || error.message); 24 | return null; 25 | } 26 | 27 | } 28 | 29 | async getSystemTime() { 30 | 31 | try { 32 | const response = await axios.get(`${process.env.API_URL}/api/v1/time`); 33 | return response.data 34 | } catch (error) { 35 | console.error('getSystemTime - ERROR!', error.response?.data || error.message); 36 | return null; 37 | } 38 | 39 | } 40 | 41 | } 42 | 43 | export default new System(); 44 | -------------------------------------------------------------------------------- /src/Backpack/Public/Trades.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | class Trades { 4 | 5 | // max limit = 1000 6 | async getRecentTrades(symbol, limit = 100) { 7 | try { 8 | const response = await axios.get(`${process.env.API_URL}/api/v1/trades`, { 9 | params:{symbol, limit}, 10 | }); 11 | return response.data 12 | } catch (error) { 13 | console.error('getRecentTrades - ERROR!', error.response?.data || error.message); 14 | return null; 15 | } 16 | 17 | } 18 | 19 | // max limit = 1000 20 | async getHistoricalTrades(symbol, limit = 100, offset = 0) { 21 | 22 | try { 23 | const response = await axios.get(`${process.env.API_URL}/api/v1/trades/history`, { 24 | params:{symbol, limit, offset}, 25 | }); 26 | return response.data 27 | } catch (error) { 28 | console.error('getHistoricalTrades - ERROR!', error.response?.data || error.message); 29 | return null; 30 | } 31 | 32 | } 33 | 34 | 35 | } 36 | 37 | export default new Trades(); 38 | -------------------------------------------------------------------------------- /src/Controllers/AccountController.js: -------------------------------------------------------------------------------- 1 | import Markets from '../Backpack/Public/Markets.js' 2 | import Account from '../Backpack/Authenticated/Account.js'; 3 | import Capital from '../Backpack/Authenticated/Capital.js'; 4 | 5 | class AccountController { 6 | 7 | 8 | async getMarkets(marketType = "PERP", orderBookState = "Open" ) { 9 | try { 10 | 11 | let markets = await Markets.getMarkets() 12 | 13 | markets = markets.filter((el) => { 14 | const matchMarketType = marketType == null || el.marketType === marketType; 15 | const matchOrderBookState = orderBookState == null || el.orderBookState === orderBookState; 16 | return matchMarketType && matchOrderBookState; 17 | }) 18 | .map((el) => { 19 | const decimal_quantity = String(el.filters.quantity.stepSize).includes(".") 20 | ? String(el.filters.quantity.stepSize).split(".")[1].length 21 | : 0; 22 | const decimal_price = String(el.filters.price.tickSize).includes(".") 23 | ? String(el.filters.price.tickSize).split(".")[1].length 24 | : 0; 25 | 26 | return { 27 | symbol: el.symbol, 28 | decimal_quantity, 29 | decimal_price, 30 | stepSize_quantity: Number(el.filters.quantity.stepSize), 31 | tickSize: Number(el.filters.price.tickSize), 32 | }; 33 | }); 34 | 35 | 36 | return markets 37 | 38 | } catch (error) { 39 | console.log(error) 40 | return null 41 | } 42 | 43 | } 44 | 45 | 46 | async get() { 47 | 48 | try { 49 | 50 | const Accounts = await Account.getAccount() 51 | const Collateral = await Capital.getCollateral() 52 | 53 | const markets = await this.getMarkets() 54 | 55 | const makerFee = parseFloat(Accounts.futuresMakerFee) / 10000 56 | const takerFee = parseFloat(Accounts.futuresTakerFee) / 10000 57 | const leverage = parseInt(Accounts.leverageLimit) 58 | const capitalAvailable = parseFloat(Collateral.netEquityAvailable) * leverage * 0.95 59 | 60 | const obj = { 61 | fee:makerFee, 62 | makerFee: makerFee, 63 | takerFee: takerFee, 64 | leverage:leverage, 65 | capitalAvailable, 66 | markets 67 | } 68 | 69 | return obj 70 | 71 | } catch (error) { 72 | console.log(error) 73 | return null 74 | } 75 | 76 | } 77 | 78 | async getallMarkets(ignore) { 79 | let markets = await Markets.getMarkets(ignore = []) 80 | 81 | markets = markets.filter((el) => 82 | el.marketType === "PERP" && 83 | el.orderBookState === "Open" && 84 | (ignore.length === 0 || !ignore.includes(el.symbol))).map((el) => { 85 | 86 | const decimal_quantity = String(el.filters.quantity.stepSize).includes(".") ? String(el.filters.quantity.stepSize.split(".")[1]).length : 0 87 | const decimal_price = String(el.filters.price.tickSize).includes(".") ? String(el.filters.price.tickSize.split(".")[1]).length : 0 88 | 89 | return { 90 | symbol: el.symbol, 91 | decimal_quantity: decimal_quantity, 92 | decimal_price: decimal_price, 93 | stepSize_quantity: Number(el.filters.quantity.stepSize), 94 | tickSize: Number(el.filters.price.tickSize) 95 | } 96 | }) 97 | 98 | return markets 99 | } 100 | 101 | } 102 | 103 | export default new AccountController(); 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/Controllers/CacheController.js: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import { open } from 'sqlite'; 3 | import path from 'path'; 4 | import AccountController from './AccountController.js'; 5 | 6 | export default class CacheController { 7 | constructor() { 8 | this.db = null; 9 | this.dbPath = path.resolve('./account_cache.db'); 10 | } 11 | 12 | async init() { 13 | this.db = await open({ 14 | filename: this.dbPath, 15 | driver: sqlite3.Database, 16 | }); 17 | 18 | await this.db.exec(` 19 | CREATE TABLE IF NOT EXISTS account_config ( 20 | id INTEGER PRIMARY KEY, 21 | fee REAL, 22 | makerFee REAL, 23 | takerFee REAL, 24 | leverage INTEGER, 25 | capitalAvailable REAL, 26 | markets TEXT, 27 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 28 | ); 29 | `); 30 | } 31 | 32 | async update() { 33 | try { 34 | const data = await AccountController.get(); 35 | if (!data) throw new Error('Erro ao obter dados da API.'); 36 | 37 | await this.init(); 38 | 39 | await this.db.run(`DELETE FROM account_config;`); 40 | 41 | await this.db.run(` 42 | INSERT INTO account_config ( 43 | fee, 44 | makerFee, 45 | takerFee, 46 | leverage, 47 | capitalAvailable, 48 | markets, 49 | updated_at 50 | ) VALUES (?, ?, ?, ?, ?, ?, datetime('now')) 51 | `, [ 52 | data.fee, 53 | data.makerFee, 54 | data.takerFee, 55 | data.leverage, 56 | data.capitalAvailable, 57 | JSON.stringify(data.markets), 58 | ]); 59 | 60 | console.log("💾 Caching is Updated"); 61 | 62 | return data; 63 | } catch (error) { 64 | console.error('Erro ao atualizar cache:', error.message); 65 | return null; 66 | } 67 | } 68 | 69 | async get() { 70 | try { 71 | await this.init(); 72 | 73 | const row = await this.db.get(`SELECT * FROM account_config LIMIT 1`); 74 | if (!row) return null; 75 | 76 | return { 77 | fee: Number(row.fee), 78 | makerFee: Number(row.makerFee), 79 | takerFee: Number(row.takerFee), 80 | leverage: Number(row.leverage), 81 | capitalAvailable: Number(parseFloat(row.capitalAvailable).toFixed(2)), 82 | markets: JSON.parse(row.markets), 83 | }; 84 | } catch (error) { 85 | console.error('Erro ao acessar cache:', error.message); 86 | return null; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Controllers/OrderController.js: -------------------------------------------------------------------------------- 1 | import Order from '../Backpack/Authenticated/Order.js'; 2 | import AccountController from './AccountController.js'; 3 | import Utils from '../Utils/Utils.js'; 4 | const tickSizeMultiply = 5 5 | 6 | class OrderController { 7 | 8 | async forceClose(position) { 9 | try { 10 | 11 | const Account = await AccountController.get() 12 | const market = Account.markets.find((el) => { 13 | return el.symbol === position.symbol 14 | }) 15 | const isLong = parseFloat(position.netQuantity) > 0; 16 | const quantity = Math.abs(parseFloat(position.netQuantity)); 17 | const decimal = market.decimal_quantity 18 | 19 | const body = { 20 | symbol: position.symbol, 21 | orderType: 'Market', 22 | side: isLong ? 'Ask' : 'Bid', // Ask if LONG , Bid if SHORT 23 | reduceOnly: true, 24 | clientId: Math.floor(Math.random() * 1000000), 25 | quantity:String(quantity.toFixed(decimal)) 26 | }; 27 | 28 | return await Order.executeOrder(body); 29 | 30 | 31 | } catch (error) { 32 | console.log(error) 33 | return null 34 | } 35 | } 36 | 37 | async openOrderSpot({ side, symbol, volume, quantity }) { 38 | 39 | 40 | 41 | try { 42 | 43 | const body = { 44 | symbol: symbol, 45 | side, 46 | orderType: "Market", 47 | timeInForce: "GTC", 48 | selfTradePrevention: "RejectTaker" 49 | }; 50 | 51 | if(quantity) { 52 | body.quantity = quantity 53 | } else { 54 | body.quoteQuantity = volume 55 | } 56 | 57 | const resp = await Order.executeOrder(body); 58 | return resp 59 | 60 | } catch (error) { 61 | console.log(error) 62 | } 63 | } 64 | 65 | async openOrder({ entry, stop, target, action, symbol, volume, decimal_quantity, decimal_price, stepSize_quantity, tickSize }) { 66 | 67 | try { 68 | 69 | const isLong = action === "long"; 70 | const side = isLong ? "Bid" : "Ask"; 71 | 72 | const formatPrice = (value) => parseFloat(value).toFixed(decimal_price).toString(); 73 | const formatQuantity = (value) => parseFloat(value).toFixed(decimal_quantity).toString(); 74 | 75 | const entryPrice = parseFloat(entry); 76 | 77 | const quantity = formatQuantity(Math.floor((volume / entryPrice) / stepSize_quantity) * stepSize_quantity); 78 | const price = formatPrice(entryPrice); 79 | 80 | const body = { 81 | symbol: symbol, 82 | side, 83 | orderType: "Limit", 84 | postOnly: true, 85 | quantity, 86 | price, 87 | timeInForce: "GTC", 88 | selfTradePrevention: "RejectTaker" 89 | }; 90 | 91 | const space = tickSize * tickSizeMultiply 92 | const takeProfitTriggerPrice = isLong ? target - space : target + space; 93 | const stopLossTriggerPrice = isLong ? stop + space : stop - space; 94 | 95 | if (target !== undefined && !isNaN(parseFloat(target))) { 96 | body.takeProfitTriggerBy = "LastPrice"; 97 | body.takeProfitTriggerPrice = formatPrice(takeProfitTriggerPrice); 98 | body.takeProfitLimitPrice = formatPrice(target); 99 | } 100 | 101 | if (stop !== undefined && !isNaN(parseFloat(stop))) { 102 | body.stopLossTriggerBy = "LastPrice"; 103 | body.stopLossTriggerPrice = formatPrice(stopLossTriggerPrice); 104 | body.stopLossLimitPrice = formatPrice(stop); 105 | } 106 | 107 | if(body.quantity > 0 && body.price > 0){ 108 | return await Order.executeOrder(body); 109 | } 110 | 111 | } catch (error) { 112 | console.log(error) 113 | } 114 | } 115 | 116 | async getRecentOpenOrders(market) { 117 | const orders = await Order.getOpenOrders(market) 118 | if(orders) { 119 | const orderShorted = orders.sort((a,b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) 120 | return orderShorted.map((el) => { 121 | el.minutes = Utils.minutesAgo(el.createdAt) 122 | el.triggerPrice = Number(el.triggerPrice), 123 | el.price = Number(el.price) 124 | return el 125 | }) 126 | } else { 127 | return [] 128 | } 129 | 130 | } 131 | 132 | 133 | 134 | async getAllOrdersSchedule(markets_open) { 135 | const orders = await Order.getOpenOrders() 136 | const orderShorted = orders.sort((a,b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) 137 | 138 | const list = orderShorted.map((el) => { 139 | return { 140 | id: el.id, 141 | minutes: Utils.minutesAgo(el.createdAt), 142 | triggerPrice: parseFloat(el.triggerPrice), 143 | symbol: el.symbol 144 | } 145 | }) 146 | 147 | return list.filter((el) => !markets_open.includes(el.symbol)) 148 | } 149 | 150 | async createStopTS({ symbol, price, isLong, quantity }) { 151 | 152 | const Account = await AccountController.get(); 153 | const find = Account.markets.find(el => el.symbol === symbol); 154 | 155 | if (!find) throw new Error(`Symbol ${symbol} not found in account data`); 156 | 157 | const decimal_quantity = find.decimal_quantity; 158 | const decimal_price = find.decimal_price; 159 | const tickSize = find.tickSize * tickSizeMultiply 160 | 161 | if (price <= 0) throw new Error("Invalid price: must be > 0"); 162 | 163 | price = Math.abs(price); 164 | 165 | const triggerPrice = isLong ? price - tickSize : price + tickSize 166 | const formatPrice = (value) => parseFloat(value).toFixed(decimal_price).toString(); 167 | const formatQuantity = (value) => parseFloat(value).toFixed(decimal_quantity).toString(); 168 | const body = { 169 | symbol, 170 | orderType: 'Limit', 171 | side: isLong ? 'Ask' : 'Bid', 172 | reduceOnly: true, 173 | postOnly: true, 174 | timeInForce: 'GTC', 175 | selfTradePrevention: "RejectTaker", 176 | price: formatPrice(price), 177 | triggerBy: 'LastPrice', 178 | triggerPrice: formatPrice(triggerPrice), 179 | triggerQuantity: formatQuantity(quantity), 180 | }; 181 | 182 | return await Order.executeOrder(body); 183 | } 184 | 185 | async createLimitOrder(symbol, side, price, quantity) { 186 | 187 | const body = { 188 | symbol, 189 | orderType: 'Limit', 190 | side, 191 | price: price.toString(), 192 | quantity: quantity.toString(), 193 | postOnly: true, 194 | reduceOnly: false, 195 | timeInForce: 'GTC', 196 | }; 197 | 198 | try { 199 | await Order.executeOrder(body); 200 | } catch (err) { 201 | console.error("❌ Erro ao criar ordem:", body , err); 202 | } 203 | } 204 | 205 | async createLimitOrderGrid(symbol, side, price, quantity, account, clientId) { 206 | 207 | const find = account.markets.find(el => el.symbol === symbol); 208 | const {decimal_price, decimal_quantity, stepSize_quantity, tickSize} = find 209 | 210 | const formatPrice = (value) => parseFloat(value).toFixed(decimal_price).toString(); 211 | const formatQuantity = (value) => parseFloat(value).toFixed(decimal_quantity).toString(); 212 | 213 | const _quantity = formatQuantity(Math.floor(quantity / stepSize_quantity) * stepSize_quantity); 214 | 215 | const isLong = side === "Ask" 216 | const triggerPrice = isLong ? price - tickSize : price + tickSize 217 | 218 | const body = { 219 | symbol, 220 | orderType: 'Limit', 221 | side, 222 | price: formatPrice(price), 223 | postOnly: true, 224 | reduceOnly: false, 225 | quantity : _quantity, 226 | timeInForce: 'GTC', 227 | clientId 228 | }; 229 | 230 | try { 231 | await Order.executeOrder(body); 232 | } catch (err) { 233 | console.error("❌ Erro ao criar ordem:", body ); 234 | } 235 | } 236 | 237 | async createLimitStop(symbol, side, price, quantity) { 238 | try { 239 | const body = { 240 | symbol, 241 | orderType: 'Limit', 242 | side, 243 | price: price.toString(), 244 | quantity: quantity.toString(), 245 | postOnly: true, 246 | reduceOnly: true, 247 | timeInForce: 'GTC', 248 | }; 249 | await Order.executeOrder(body); 250 | } catch (err) { 251 | console.error("❌ Erro ao criar ordem:", err.message); 252 | } 253 | } 254 | 255 | async createLimitTriggerStop(symbol, side, price, quantity, account, markPrice) { 256 | 257 | 258 | try { 259 | const find = account.markets.find(el => el.symbol === symbol); 260 | const {decimal_price, decimal_quantity, stepSize_quantity} = find 261 | const formatPrice = (value) => parseFloat(value).toFixed(decimal_price).toString(); 262 | const formatQuantity = (value) => parseFloat(value).toFixed(decimal_quantity).toString(); 263 | const qnt = formatQuantity(Math.floor((quantity) / stepSize_quantity) * stepSize_quantity); 264 | 265 | const tickSize = Number(find.tickSize) * tickSizeMultiply 266 | const isLong = side === "Ask" 267 | const triggerPrice = isLong ? price + tickSize : price - tickSize 268 | 269 | const body = { 270 | symbol, 271 | orderType: 'Limit', 272 | side, 273 | price: formatPrice(price), 274 | postOnly: true, 275 | reduceOnly: true, 276 | timeInForce: 'GTC', 277 | triggerBy :'LastPrice', 278 | triggerPrice : formatPrice(triggerPrice), 279 | triggerQuantity : qnt 280 | }; 281 | 282 | 283 | await Order.executeOrder(body); 284 | } catch (err) { 285 | console.error("❌ Erro ao criar ordem:", err.message); 286 | } 287 | } 288 | 289 | async createMarketStop(symbol, side, price, quantity) { 290 | try { 291 | const body = { 292 | symbol, 293 | orderType: 'Market', 294 | side, 295 | price: price.toString(), 296 | quantity: quantity.toString(), 297 | reduceOnly: true, 298 | postOnly: true, 299 | timeInForce: 'GTC', 300 | }; 301 | await Order.executeOrder(body); 302 | } catch (err) { 303 | console.error("❌ Erro ao criar ordem:", err.message); 304 | } 305 | } 306 | 307 | } 308 | 309 | export default new OrderController(); 310 | 311 | 312 | -------------------------------------------------------------------------------- /src/Controllers/PnlController.js: -------------------------------------------------------------------------------- 1 | import History from '../Backpack/Authenticated/History.js'; 2 | import AccountController from './AccountController.js' 3 | 4 | class PnlController { 5 | 6 | async getVolumeMarket(symbol, date) { 7 | const from = new Date(date).getTime() 8 | const fills = await this.getFillHistory(from, symbol) 9 | if(fills) { 10 | const result = this.summarizeTrades(fills) 11 | return result 12 | } 13 | return null 14 | } 15 | 16 | getSeasonWeek() { 17 | const now = new Date(); 18 | const seasonStart = new Date('2025-07-03T00:00:00'); 19 | 20 | if (now < seasonStart) { 21 | return "Dont Start Season 2"; 22 | } 23 | 24 | // Calcula a diferença em milissegundos e converte para dias 25 | const diffMs = now - seasonStart; 26 | const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); 27 | 28 | // Calcula a semana (semana 1 = dias 0 a 6, semana 2 = dias 7 a 13, etc.) 29 | const weekNumber = Math.floor(diffDays / 7) + 1; 30 | 31 | if (weekNumber > 10) { 32 | return "Season 2 End"; 33 | } 34 | 35 | return `Week ${weekNumber} of 10`; 36 | } 37 | 38 | millisSinceLastWednesday23() { 39 | const now = new Date(); 40 | const dayOfWeek = now.getDay(); // domingo=0, segunda=1, ..., quarta=3 41 | const daysSinceWednesday = (dayOfWeek + 7 - 3) % 7; 42 | 43 | // Cria data da última quarta-feira 44 | const lastWednesday = new Date(now); 45 | lastWednesday.setDate(now.getDate() - daysSinceWednesday); 46 | lastWednesday.setHours(23, 0, 0, 0); 47 | 48 | // Se hoje for quarta e ainda não passou das 23h, volta mais 7 dias 49 | if (dayOfWeek === 3 && now < lastWednesday) { 50 | lastWednesday.setDate(lastWednesday.getDate() - 7); 51 | } 52 | 53 | const diff = now - lastWednesday; 54 | 55 | return diff; 56 | } 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | async start(TRADING_STRATEGY) { 70 | try { 71 | const week = this.getSeasonWeek() 72 | const milissegundos = this.millisSinceLastWednesday23() 73 | const date = Date.now() - (milissegundos); 74 | const fills = await this.getFillHistory(date) 75 | if(fills) { 76 | const result = this.summarizeTrades(fills) 77 | const VOLUME_BY_POINT = Number(process.env.VOLUME_BY_POINT) 78 | const points = parseInt(result.totalVolume / VOLUME_BY_POINT) 79 | const Account = await AccountController.get() 80 | console.log("") 81 | console.log("=========================== Wellcome Backbot v2 🤖 ===========================") 82 | console.log("") 83 | console.log(`✨ ${week} ✨`) 84 | console.log("") 85 | console.log("💸 Fees", Number(parseFloat(result.totalFee).toFixed(2))) 86 | console.log("💰 Volume", Number(parseFloat(result.totalVolume).toFixed(0))) 87 | console.log("👀 Volume by 1 fee dol", result.volumeBylFee ? Number(parseFloat(result.volumeBylFee).toFixed(2)) : 0) 88 | console.log(`📈 Leverage`,Account.leverage) 89 | console.log(`🔮 Estimated points`, points) 90 | console.log(`🎮 Selected strategy ${TRADING_STRATEGY}`) 91 | console.log("") 92 | console.log("==================== Powered by https://x.com/heronjr_x =======================") 93 | console.log("") 94 | console.log("") 95 | } 96 | 97 | 98 | } catch (error) { 99 | console.log(error) 100 | } 101 | } 102 | 103 | 104 | async getFillHistory(from, symbol = null) { 105 | const to = Date.now(); 106 | const orderId = null; 107 | const limit = 1000; 108 | const fillType = null; 109 | const marketType = null; 110 | const sortDirection = 'Desc'; 111 | 112 | let offset = 0; 113 | let allFills = []; 114 | 115 | while (true) { 116 | const fills = await History.getFillHistory( 117 | symbol, 118 | orderId, 119 | from, 120 | to, 121 | limit, 122 | offset, 123 | fillType, 124 | marketType, 125 | sortDirection 126 | ); 127 | 128 | if (!fills || fills.length === 0) { 129 | break; 130 | } 131 | 132 | allFills.push(...fills); 133 | 134 | if (fills.length < limit) { 135 | break; 136 | } 137 | 138 | offset += limit; 139 | } 140 | 141 | return allFills; 142 | } 143 | 144 | 145 | 146 | async run(hour = 24) { 147 | try { 148 | const oneDayAgo = Date.now() - hour * 60 * 60 * 1000; 149 | const fills = await this.getFillHistory(oneDayAgo) 150 | 151 | if(fills) { 152 | const VOLUME_BY_POINT = Number(process.env.VOLUME_BY_POINT) 153 | const result = this.summarizeTrades(fills) 154 | const points = parseInt(result.totalVolume / VOLUME_BY_POINT) 155 | console.log(`✨ Last ${hour} hour(s) ✨`) 156 | console.log("💸 Fees", Number(parseFloat(result.totalFee).toFixed(2))) 157 | console.log("💰 Volume", Number(parseFloat(result.totalVolume).toFixed(0))) 158 | console.log("👀 Volume by 1 fee dol", result.volumeBylFee ? Number(parseFloat(result.volumeBylFee).toFixed(2)) : 0) 159 | console.log(`🔮 Estimated points`, points) 160 | console.log("") 161 | console.log("") 162 | } 163 | 164 | 165 | } catch (error) { 166 | console.log(error) 167 | } 168 | } 169 | 170 | summarizeTrades(trades) { 171 | try { 172 | const bySymbol = trades.reduce((acc, { symbol, price, quantity, fee, side }) => { 173 | const p = parseFloat(price); 174 | const q = parseFloat(quantity); 175 | const f = parseFloat(fee); 176 | const volume = p * q; 177 | const pnl = side === 'Ask' ? volume : -volume; 178 | 179 | if (!acc[symbol]) { 180 | acc[symbol] = { totalFee: 0, totalVolume: 0, totalPnl: 0 }; 181 | } 182 | 183 | acc[symbol].totalFee += f; 184 | acc[symbol].totalVolume += volume; 185 | acc[symbol].totalPnl += pnl; 186 | return acc; 187 | }, {}); 188 | 189 | const overall = Object.values(bySymbol).reduce( 190 | (tot, curr) => ({ 191 | totalFee: tot.totalFee + curr.totalFee, 192 | totalVolume: tot.totalVolume + curr.totalVolume 193 | }), 194 | { totalFee: 0, totalVolume: 0 } 195 | ); 196 | 197 | const volumeBylFee = (overall.totalVolume / overall.totalFee ) 198 | 199 | return { 200 | totalFee: overall.totalFee, 201 | totalVolume: overall.totalVolume, 202 | volumeBylFee: isNaN(volumeBylFee) ? null : volumeBylFee 203 | }; 204 | 205 | } catch (error) { 206 | console.log(error) 207 | return null 208 | } 209 | } 210 | 211 | } 212 | export default new PnlController(); -------------------------------------------------------------------------------- /src/Decision/Decision.js: -------------------------------------------------------------------------------- 1 | import Futures from '../Backpack/Authenticated/Futures.js'; 2 | import Order from '../Backpack/Authenticated/Order.js'; 3 | import OrderController from '../Controllers/OrderController.js'; 4 | import AccountController from '../Controllers/AccountController.js'; 5 | import Markets from '../Backpack/Public/Markets.js'; 6 | import { calculateIndicators } from './Indicators.js'; 7 | import CacheController from '../Controllers/CacheController.js'; 8 | import Terminal from '../Utils/Terminal.js'; 9 | import dotenv from 'dotenv'; 10 | dotenv.config(); 11 | const Cache = new CacheController(); 12 | 13 | class Decision { 14 | 15 | constructor() { 16 | this.UNIQUE_TREND = String(process.env.UNIQUE_TREND).toUpperCase().trim() 17 | this.MAX_ORDER_OPEN = Number(process.env.MAX_ORDER_OPEN) 18 | } 19 | 20 | async getDataset(Account, closed_markets) { 21 | const dataset = [] 22 | const AUTHORIZED_MARKET = JSON.parse(process.env.AUTHORIZED_MARKET); 23 | 24 | const markets = Account.markets.filter(el => { 25 | const isOpen = !closed_markets.includes(el.symbol); 26 | const isAuthorized = AUTHORIZED_MARKET.length === 0 || AUTHORIZED_MARKET.includes(el.symbol); 27 | return isOpen && isAuthorized; 28 | }); 29 | 30 | try { 31 | 32 | Terminal.init(markets.length, markets.length) 33 | let count = 0 34 | 35 | for (const market of markets) { 36 | 37 | const candles_1m = await Markets.getKLines(market.symbol, "1m", 30) 38 | const candles_5m = await Markets.getKLines(market.symbol, "5m", 30) 39 | const candles_15m = await Markets.getKLines(market.symbol, "15m", 30) 40 | 41 | const analyze_1m = calculateIndicators(candles_1m) 42 | const analyze_5m = calculateIndicators(candles_5m) 43 | const analyze_15m = calculateIndicators(candles_15m) 44 | 45 | const getAllMarkPrices = await Markets.getAllMarkPrices(market.symbol) 46 | const marketPrice = getAllMarkPrices[0].markPrice 47 | 48 | count++ 49 | Terminal.update(`🔍 Analyzing ${markets.length} Markets`, count) 50 | 51 | const obj = { 52 | market, 53 | marketPrice, 54 | "1m":analyze_1m, 55 | "5m":analyze_5m, 56 | "15m":analyze_15m 57 | } 58 | 59 | dataset.push(obj) 60 | } 61 | 62 | Terminal.finish() 63 | 64 | } catch (error) { 65 | console.log(error) 66 | } 67 | 68 | return dataset 69 | } 70 | 71 | evaluateTradeOpportunity(obj) { 72 | const { market, marketPrice, "1m": tf1, "5m": tf5, "15m": tf15 } = obj; 73 | const mp = parseFloat(marketPrice); 74 | 75 | function scoreSide(isLong) { 76 | let score = 0; 77 | let total = 9; 78 | 79 | if (tf15.ema.ema9 > tf15.ema.ema21 === isLong) score++; 80 | if (tf5.ema.ema9 > tf5.ema.ema21 === isLong) score++; 81 | 82 | if ((isLong && tf5.rsi.value > 55) || (!isLong && tf5.rsi.value < 45)) score++; 83 | 84 | if (tf5.macd.MACD > tf5.macd.MACD_signal === isLong) score++; 85 | 86 | const boll = tf1.bollinger; 87 | if ((isLong && mp > boll.BOLL_middle) || (!isLong && mp < boll.BOLL_middle)) score++; 88 | 89 | if ((isLong && mp > tf1.vwap.vwap) || (!isLong && mp < tf1.vwap.vwap)) score++; 90 | 91 | if (tf1.volume.volume.trend === "increasing") score++; 92 | 93 | if ( 94 | (isLong && tf1.volume.price.slope > 0) || 95 | (!isLong && tf1.volume.price.slope < 0) 96 | ) score++; 97 | 98 | const stack = ( 99 | (isLong && tf5.rsi.value > 55 && tf5.macd.MACD > tf5.macd.MACD_signal && tf5.ema.ema9 > tf5.ema.ema21) || 100 | (!isLong && tf5.rsi.value < 45 && tf5.macd.MACD < tf5.macd.MACD_signal && tf5.ema.ema9 < tf5.ema.ema21) 101 | ); 102 | if (stack) score++; 103 | 104 | return Math.round((score / total) * 100); 105 | } 106 | 107 | const longScore = scoreSide(true); 108 | const shortScore = scoreSide(false); 109 | 110 | const isLong = longScore > shortScore; 111 | const certainty = Math.max(longScore, shortScore); 112 | 113 | const entry = isLong 114 | ? mp - (market.tickSize * 10) 115 | : mp + (market.tickSize * 10) 116 | 117 | return { 118 | side: isLong ? "long" : "short", 119 | certainty: certainty, 120 | ...market, 121 | entry: parseFloat(entry.toFixed(obj.market.decimal_price)), 122 | }; 123 | 124 | } 125 | 126 | async openOrder(row) { 127 | const orders = await OrderController.getRecentOpenOrders(row.symbol) 128 | const [firstOrder] = orders; 129 | 130 | if (firstOrder) { 131 | if(firstOrder.minutes > 3) { 132 | await Order.cancelOpenOrders(row.symbol) 133 | await OrderController.openOrder(row) 134 | } 135 | } else { 136 | await OrderController.openOrder(row) 137 | } 138 | } 139 | 140 | async analyze() { 141 | 142 | try { 143 | 144 | const account = await Cache.get() 145 | 146 | if(account.leverage > 10){ 147 | console.log(`Leverage ${account.leverage}x HIGH RISK LIQUIDATION ☠️`) 148 | } 149 | 150 | const positions = await Futures.getOpenPositions() 151 | 152 | const open_markers = positions.map((el) => el.symbol) 153 | 154 | const orders = await Order.getOpenOrders(null, "PERP") 155 | 156 | for (const order of orders) { 157 | if(!open_markers.includes(order.symbol)){ 158 | open_markers.push(order.symbol) 159 | } 160 | } 161 | 162 | console.log("open_markers.length", open_markers.length) 163 | 164 | if(this.MAX_ORDER_OPEN > open_markers.length){ 165 | 166 | const VOLUME_ORDER = Number(process.env.VOLUME_ORDER) 167 | 168 | if(VOLUME_ORDER < account.capitalAvailable){ 169 | 170 | if(positions){ 171 | 172 | const closed_markets = positions.map((el) => el.symbol) 173 | const dataset = await this.getDataset(account, closed_markets) 174 | const CERTAINTY = Number(process.env.CERTAINTY) 175 | const rows = dataset.map((row) => this.evaluateTradeOpportunity(row)).filter((el) => el.certainty >= CERTAINTY) 176 | 177 | for (const row of rows) { 178 | row.volume = VOLUME_ORDER 179 | row.action = row.side 180 | 181 | const isLong = row.side === "long" 182 | const MAX_PERCENT_LOSS = Number(String(process.env.MAX_PERCENT_LOSS).replace("%","")) / 100 183 | const MAX_PERCENT_PROFIT = Number(String(process.env.MAX_PERCENT_PROFIT).replace("%","")) / 100 184 | 185 | const quantity = (VOLUME_ORDER / row.entry) 186 | const fee_open = VOLUME_ORDER * account.fee 187 | const fee_total_loss = (fee_open + (fee_open * MAX_PERCENT_LOSS)) / quantity 188 | const fee_total_profit = (fee_open + (fee_open * MAX_PERCENT_PROFIT)) / quantity 189 | 190 | const stop = isLong ? (row.entry - (row.entry * MAX_PERCENT_LOSS)) - fee_total_loss : (row.entry + (row.entry * MAX_PERCENT_LOSS)) + fee_total_loss 191 | const target = isLong ? (row.entry + (row.entry * MAX_PERCENT_PROFIT)) + fee_total_profit : (row.entry - (row.entry * MAX_PERCENT_PROFIT)) - fee_total_profit 192 | 193 | row.stop = stop 194 | row.target = target 195 | 196 | if(this.UNIQUE_TREND !== ""){ 197 | if((this.UNIQUE_TREND === "LONG" && isLong === true) || (this.UNIQUE_TREND === "SHORT" && isLong === false)) { 198 | await this.openOrder(row) 199 | } else { 200 | console.log( row.symbol, "Ignore by rule UNIQUE_TREND active Only", this.UNIQUE_TREND) 201 | } 202 | } else { 203 | await this.openOrder(row) 204 | } 205 | 206 | } 207 | } 208 | 209 | } 210 | 211 | } 212 | 213 | 214 | } catch (error) { 215 | console.log(error) 216 | } 217 | 218 | } 219 | 220 | } 221 | 222 | export default new Decision(); -------------------------------------------------------------------------------- /src/Decision/Indicators.js: -------------------------------------------------------------------------------- 1 | import { EMA, RSI, MACD, BollingerBands, VWAP } from 'technicalindicators'; 2 | 3 | function calculateVWAPClassicBands(candles) { 4 | let sumVol = 0; 5 | let sumTPV = 0; 6 | 7 | // 1ª passada: soma de volume e tp * volume 8 | for (const c of candles) { 9 | const high = parseFloat(c.high); 10 | const low = parseFloat(c.low); 11 | const close = parseFloat(c.close); 12 | const vol = parseFloat(c.volume); 13 | 14 | const tp = (high + low + close) / 3; 15 | sumVol += vol; 16 | sumTPV += tp * vol; 17 | } 18 | 19 | const vwap = sumTPV / sumVol; 20 | 21 | // 2ª passada: soma do desvio² ponderado por volume 22 | let sumVarV = 0; 23 | for (const c of candles) { 24 | const high = parseFloat(c.high); 25 | const low = parseFloat(c.low); 26 | const close = parseFloat(c.close); 27 | const vol = parseFloat(c.volume); 28 | 29 | const tp = (high + low + close) / 3; 30 | const diff = tp - vwap; 31 | sumVarV += vol * diff * diff; 32 | } 33 | 34 | const variance = sumVarV / sumVol; 35 | const stdDev = Math.sqrt(variance); 36 | 37 | // bandas clássicas: ±1, ±2, ±3 desvios 38 | const upperBands = [ 39 | vwap + stdDev, 40 | vwap + 2 * stdDev, 41 | vwap + 3 * stdDev 42 | ]; 43 | const lowerBands = [ 44 | vwap - stdDev, 45 | vwap - 2 * stdDev, 46 | vwap - 3 * stdDev 47 | ]; 48 | 49 | return { 50 | vwap, 51 | stdDev, 52 | upperBands, 53 | lowerBands 54 | }; 55 | } 56 | 57 | function findEMACross(ema9Arr, ema21Arr) { 58 | const len = Math.min(ema9Arr.length, ema21Arr.length); 59 | 60 | for (let i = len - 2; i >= 0; i--) { 61 | const currEma9 = ema9Arr[i + 1]; 62 | const prevEma9 = ema9Arr[i]; 63 | const currEma21 = ema21Arr[i + 1]; 64 | const prevEma21 = ema21Arr[i]; 65 | 66 | // Detecta cruzamentos 67 | if (prevEma9 <= prevEma21 && currEma9 > currEma21) { 68 | return { index: i, type: 'goldenCross' }; 69 | } 70 | if (prevEma9 >= prevEma21 && currEma9 < currEma21) { 71 | return { index: i, type: 'deathCross' }; 72 | } 73 | } 74 | 75 | return null; 76 | } 77 | 78 | 79 | function analyzeEMA(ema9Arr, ema21Arr) { 80 | const len = ema9Arr.length; 81 | if (len < 2 || ema21Arr.length < 2) return null; 82 | 83 | const lastEma9 = ema9Arr.at(-1); 84 | const lastEma21 = ema21Arr.at(-1); 85 | const prevEma9 = ema9Arr.at(-2); 86 | const prevEma21 = ema21Arr.at(-2); 87 | 88 | if (lastEma9 == null || lastEma21 == null || prevEma9 == null || prevEma21 == null) { 89 | return null; 90 | } 91 | 92 | // diferença absoluta e percentual 93 | const diff = lastEma9 - lastEma21; 94 | const diffPct = (diff / lastEma21) * 100; 95 | 96 | // sinal básico 97 | const signal = diff > 0 ? 'bullish' : 'bearish'; 98 | 99 | // detectar cruzamento no último candle 100 | let crossed = null; 101 | if (prevEma9 <= prevEma21 && lastEma9 > lastEma21) { 102 | crossed = 'goldenCross'; 103 | } else if (prevEma9 >= prevEma21 && lastEma9 < lastEma21) { 104 | crossed = 'deathCross'; 105 | } 106 | 107 | return { 108 | ema9: lastEma9, 109 | ema21: lastEma21, 110 | diff, 111 | diffPct, 112 | signal, 113 | crossed 114 | }; 115 | } 116 | 117 | function analyzeTrends(data) { 118 | const n = data.length; 119 | const result = {}; 120 | const metrics = ['volume', 'variance', 'price']; 121 | 122 | // soma dos índices de 0 a n-1 e sum(x^2) podem ser pré-calculados 123 | const sumX = (n - 1) * n / 2; 124 | const sumXX = (n - 1) * n * (2 * n - 1) / 6; 125 | 126 | metrics.forEach((metric) => { 127 | let sumY = 0; 128 | let sumXY = 0; 129 | 130 | data.forEach((d, i) => { 131 | const y = d[metric]; 132 | sumY += y; 133 | sumXY += i * y; 134 | }); 135 | 136 | // slope = (n * Σ(xᵢyᵢ) - Σxᵢ * Σyᵢ) / (n * Σ(xᵢ²) - (Σxᵢ)²) 137 | const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); 138 | 139 | // intercept = mean(y) - slope * mean(x) 140 | const intercept = (sumY / n) - slope * (sumX / n); 141 | 142 | // previsão para o próximo ponto (índice n) 143 | const forecast = slope * n + intercept; 144 | 145 | // tendência 146 | const trend = slope > 0 147 | ? 'increasing' 148 | : slope < 0 149 | ? 'decreasing' 150 | : 'flat'; 151 | 152 | result[metric] = { 153 | trend, 154 | slope, 155 | forecast 156 | }; 157 | }); 158 | 159 | return result; 160 | } 161 | 162 | export function calculateIndicators(candles) { 163 | const closes = candles.map(c => parseFloat(c.close)); 164 | 165 | const volumesUSD = candles.map(c => ({ 166 | volume: parseFloat(c.quoteVolume), 167 | variance: parseFloat(c.high) - parseFloat(c.low), 168 | price: parseFloat(c.start) - parseFloat(c.close), 169 | })); 170 | 171 | const ema9 = EMA.calculate({ period: 9, values: closes }); 172 | const ema21 = EMA.calculate({ period: 21, values: closes }); 173 | 174 | const rsi = RSI.calculate({ period: 14, values: closes }); 175 | 176 | const macd = MACD.calculate({ 177 | values: closes, 178 | fastPeriod: 12, 179 | slowPeriod: 26, 180 | signalPeriod: 9, 181 | SimpleMAOscillator: false, 182 | SimpleMASignal: false 183 | }); 184 | 185 | const boll = BollingerBands.calculate({ 186 | period: 20, 187 | values: closes, 188 | stdDev: 2 189 | }); 190 | 191 | const { vwap, stdDev, upperBands, lowerBands } = calculateVWAPClassicBands(candles); 192 | const volumeAnalyse = analyzeTrends(volumesUSD) 193 | 194 | const emaAnalysis = analyzeEMA(ema9, ema21); 195 | const emaCrossInfo = findEMACross(ema9, ema21); 196 | 197 | return { 198 | ema: { 199 | ...emaAnalysis, 200 | crossIndex: emaCrossInfo?.index ?? null, 201 | crossType: emaCrossInfo?.type ?? null, 202 | candlesAgo: emaCrossInfo ? (ema9.length - 1 - emaCrossInfo.index) : null 203 | }, 204 | rsi: { 205 | value: rsi.at(-1) ?? null, 206 | history: rsi 207 | }, 208 | macd: { 209 | MACD: macd.at(-1)?.MACD ?? null, 210 | MACD_signal: macd.at(-1)?.signal ?? null, 211 | MACD_histogram: macd.at(-1)?.histogram ?? null, 212 | }, 213 | bollinger: { 214 | BOLL_upper: boll.at(-1)?.upper ?? null, 215 | BOLL_middle: boll.at(-1)?.middle ?? null, 216 | BOLL_lower: boll.at(-1)?.lower ?? null, 217 | }, 218 | volume: { 219 | history: volumesUSD, 220 | ...volumeAnalyse 221 | }, 222 | vwap: { 223 | vwap, 224 | stdDev, 225 | upperBands, 226 | lowerBands 227 | } 228 | }; 229 | } 230 | 231 | export function analyzeTrade(fee, data, volume, media_rsi) { 232 | try { 233 | 234 | if (!data.vwap?.lowerBands?.length || !data.vwap?.upperBands?.length || data.vwap.vwap == null) return null; 235 | 236 | const IsCrossBulligh = data.ema.crossType < 'goldenCross' && data.ema.candlesAgo < 2 237 | const IsCrossBearish = data.ema.crossType < 'deathCross' && data.ema.candlesAgo < 2 238 | 239 | const isReversingUp = data.rsi.value > 35 && media_rsi < 30; 240 | const isReversingDown = data.rsi.value < 65 && media_rsi > 70; 241 | 242 | const isBullish = data.ema.ema9 > data.ema.ema21 && data.ema.diffPct > 0.1; 243 | const isBearish = data.ema.ema9 < data.ema.ema21 && data.ema.diffPct < -0.1; 244 | 245 | const isRSIBullish = data.rsi.value > 50 && media_rsi > 40; 246 | const isRSIBearish = data.rsi.value < 50 && media_rsi < 60; 247 | 248 | const isLong = (isBullish && isRSIBullish) || isReversingUp || IsCrossBulligh; 249 | const isShort = (isBearish && isRSIBearish) || isReversingDown || IsCrossBearish; 250 | 251 | if (!isLong && !isShort) return null; 252 | 253 | const action = isLong ? 'long' : 'short'; 254 | const price = parseFloat(data.marketPrice); 255 | 256 | // Unifica e ordena as bandas 257 | const bands = [...data.vwap.lowerBands, ...data.vwap.upperBands].map(Number).sort((a, b) => a - b); 258 | 259 | // Encontra a banda abaixo e acima mais próximas 260 | const bandBelow = bands.filter(b => b < price); // última abaixo 261 | const bandAbove = bands.filter(b => b > price); // primeira acima 262 | 263 | if(bandAbove.length === 0 || bandBelow.length === 0) return null 264 | 265 | const entry = price; // ajuste de slippage otimista 266 | 267 | let stop, target; 268 | const percentVwap = 0.95 269 | 270 | if (isLong) { 271 | stop = bandBelow[bandBelow.length - 1] 272 | target = entry + ((bandAbove[0] - entry) * percentVwap) 273 | } else { 274 | stop = bandAbove[bandAbove.length - 1] 275 | target = entry - ((entry - bandBelow[0]) * percentVwap) 276 | } 277 | 278 | // Cálculo de PnL e risco 279 | const units = volume / entry; 280 | 281 | const grossLoss = ((action === 'long') ? entry - stop : stop - entry ) * units 282 | const grossTarget = ((action === 'long') ? target - entry : entry - target) * units 283 | 284 | const entryFee = volume * fee; 285 | const exitFeeTarget = grossTarget * fee; 286 | const exitFeeLoss = grossLoss * fee; 287 | 288 | const pnl = grossTarget - (entryFee + exitFeeTarget) 289 | const risk = grossLoss + (entryFee + exitFeeLoss) 290 | 291 | return { 292 | market: data.market.symbol, 293 | entry: Number(entry.toFixed(data.market.decimal_price)), 294 | stop: Number(stop.toFixed(data.market.decimal_price)), 295 | target: Number(target.toFixed(data.market.decimal_price)), 296 | action, 297 | pnl: Number(pnl), 298 | risk: Number(risk) 299 | }; 300 | 301 | } catch (error) { 302 | console.log(error) 303 | return null 304 | } 305 | 306 | } 307 | 308 | 309 | -------------------------------------------------------------------------------- /src/Grid/Grid.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import OrderController from '../Controllers/OrderController.js'; 3 | import AccountController from '../Controllers/AccountController.js'; 4 | import Order from '../Backpack/Authenticated/Order.js'; 5 | import Markets from '../Backpack/Public/Markets.js' 6 | import CacheController from '../Controllers/CacheController.js'; 7 | import Futures from '../Backpack/Authenticated/Futures.js'; 8 | import WebSocket from 'ws'; 9 | import { auth } from '../Backpack/Authenticated/Authentication.js'; 10 | 11 | const Cache = new CacheController(); 12 | dotenv.config(); 13 | 14 | class Grid { 15 | 16 | constructor() { 17 | this.symbol = process.env.GRID_MARKET; 18 | this.lowerPrice = parseFloat(process.env.LOWER_PRICE); 19 | this.upperPrice = parseFloat(process.env.UPPER_PRICE); 20 | this.numGrids = parseInt(process.env.NUMBER_OF_GRIDS); 21 | this.upperClose = parseInt(process.env.UPPER_FORCE_CLOSE); 22 | this.lowerClose = parseInt(process.env.LOWER_FORCE_CLOSE); 23 | this.gridPnl = parseInt(process.env.GRID_PNL); 24 | this.gridStep = (this.upperPrice - this.lowerPrice) / this.numGrids; 25 | this.orders = []; 26 | this.wsPrivate = null; 27 | this.wsPublic = null; 28 | } 29 | 30 | generateGridOrders(lastPrice) { 31 | this.orders = []; 32 | for (let i = 0; i < this.numGrids; i++) { 33 | const price = Number((this.lowerPrice + i * this.gridStep).toFixed(6)); 34 | const side = price < lastPrice ? 'Bid' : 'Ask'; 35 | this.orders.push({ price, side, clientId: i }); 36 | } 37 | } 38 | 39 | async cancelAllOrders() { 40 | try { 41 | await Order.cancelOpenOrders(this.symbol); 42 | console.log(`🧹 Cancelled existing orders for ${this.symbol}`); 43 | } catch (err) { 44 | console.error('❌ Error while cancelling orders:', err.message); 45 | } 46 | } 47 | 48 | async placeGridOrders() { 49 | const account = await Cache.get() 50 | 51 | for (const { price, side, clientId } of this.orders) { 52 | 53 | const quantityPerGrid = (account.capitalAvailable / this.numGrids) / price 54 | 55 | try { 56 | await OrderController.createLimitOrderGrid( 57 | this.symbol, 58 | side, 59 | price, 60 | quantityPerGrid, 61 | account, 62 | clientId 63 | ); 64 | console.log(`📌 ${side} @ ${price} [${clientId}]`); 65 | } catch (err) { 66 | console.error(`❌ Failed to create order ${price}:`, err.message); 67 | } 68 | } 69 | } 70 | 71 | async run() { 72 | const [markPriceData] = await Markets.getAllMarkPrices(this.symbol); 73 | const lastPrice = Number(markPriceData.markPrice); 74 | this.generateGridOrders(lastPrice); 75 | await this.cancelAllOrders(); 76 | await this.placeGridOrders(); 77 | this.connectPrivate(); 78 | this.connectPublic(); 79 | 80 | setInterval(() => { 81 | console.log("Runing private", !this.wsPrivate || this.wsPrivate.readyState !== WebSocket.OPEN ) 82 | console.log("Runing public", !this.wsPublic || this.wsPublic.readyState !== WebSocket.OPEN ) 83 | if (!this.wsPrivate || this.wsPrivate.readyState !== WebSocket.OPEN) { 84 | console.warn('⚠️ wsPrivate inativo. Reconectando...'); 85 | this.connectPrivate(); 86 | } 87 | 88 | if (!this.wsPublic || this.wsPublic.readyState !== WebSocket.OPEN) { 89 | console.warn('⚠️ wsPublic inativo. Reconectando...'); 90 | this.connectPublic(); 91 | } 92 | }, 30_000); 93 | } 94 | 95 | async handleOrderFill() { 96 | 97 | // 1. Pega o markPrice atual 98 | const [markPriceData] = await Markets.getAllMarkPrices(this.symbol); 99 | const lastPrice = Number(markPriceData.markPrice); 100 | const account = await Cache.get(); 101 | 102 | // 2. Verifica todas as ordens planejadas 103 | for (const order of this.orders) { 104 | const exists = await Order.getOpenOrder(this.symbol, null, order.clientId); 105 | 106 | if (!exists) { 107 | const side = order.price < lastPrice ? 'Bid' : 'Ask'; 108 | const quantity = (account.capitalAvailable / this.numGrids) / order.price; 109 | 110 | try { 111 | await OrderController.createLimitOrderGrid( 112 | this.symbol, 113 | side, 114 | order.price, 115 | quantity, 116 | account, 117 | order.clientId 118 | ); 119 | console.log(`🔁 Order recreated ${side} @ ${order.price} [${order.clientId}]`); 120 | } catch (err) { 121 | console.error(`❌ Error while recreating order [${order.clientId}]:`, err.message); 122 | } 123 | } 124 | } 125 | } 126 | 127 | async forceClose(symbol) { 128 | const positions = await Futures.getOpenPositions(); 129 | const position = positions.find((el) => el.symbol === symbol); 130 | 131 | if(position){ 132 | 133 | const markPrice = Number(position.markPrice) 134 | const netExposureNotional = Number(position.netExposureNotional) 135 | const account = await Cache.get() 136 | const closeFee = netExposureNotional * Number(account.makerFee) 137 | const openFee = netExposureNotional * Number(account.takerFee) 138 | const totalFee = closeFee + openFee 139 | const pnl = (Number(position.pnlRealized) + Number(position.pnlUnrealized)) - totalFee 140 | if (markPrice >= this.upperClose || markPrice <= this.lowerClose || this.gridPnl <= pnl) { 141 | await OrderController.forceClose(position); 142 | console.log(`🔒 Position forced to close ${markPrice}`); 143 | this.run() 144 | } 145 | 146 | } 147 | 148 | } 149 | 150 | connectPrivate() { 151 | this.wsPrivate = new WebSocket('wss://ws.backpack.exchange'); 152 | 153 | this.wsPrivate.on('open', () => { 154 | console.log('✅ Private WebSocket connected [Grid Mode]'); 155 | const timestamp = Date.now(); 156 | const window = 10000; 157 | const instruction = 'subscribe'; 158 | const params = {}; 159 | const headers = auth({ instruction, params, timestamp, window }); 160 | 161 | const payload = { 162 | method: 'SUBSCRIBE', 163 | params: ['account.positionUpdate', 'account.orderUpdate'], 164 | signature: [ 165 | headers['X-API-Key'], 166 | headers['X-Signature'], 167 | headers['X-Timestamp'], 168 | headers['X-Window'] 169 | ] 170 | }; 171 | 172 | this.wsPrivate.send(JSON.stringify(payload)); 173 | }); 174 | 175 | this.wsPrivate.on('message', async (raw) => { 176 | try { 177 | const parsed = JSON.parse(raw); 178 | console.log("parsed.stream", parsed.stream) 179 | if (parsed.stream === 'account.positionUpdate') { 180 | await this.handleOrderFill(parsed.data); 181 | } 182 | 183 | if (parsed.stream === 'account.orderUpdate') { 184 | const event = parsed.data; 185 | if (event.e === 'orderFill' || event.e === 'orderCancel') { 186 | await this.handleOrderFill(parsed.data); 187 | } 188 | } 189 | 190 | } catch (err) { 191 | console.error('❌ Error processing order:', err.message); 192 | } 193 | }); 194 | 195 | this.wsPrivate.on('close', () => { 196 | console.warn('🔌 Private WebSocket disconnected. Reconnecting...'); 197 | setTimeout(() => this.connectPrivate(), 3000); 198 | }); 199 | 200 | this.wsPrivate.on('error', (err) => { 201 | console.error('❌ Error on private WebSocket:', err.message); 202 | }); 203 | } 204 | 205 | connectPublic() { 206 | this.wsPublic = new WebSocket('wss://ws.backpack.exchange'); 207 | 208 | this.wsPublic.on('open', () => { 209 | console.log('🌐 Public WebSocket connected [Grid Mode]'); 210 | const payload = { 211 | method: 'SUBSCRIBE', 212 | params: [`markPrice.${this.symbol}`], 213 | }; 214 | this.wsPublic.send(JSON.stringify(payload)); 215 | }); 216 | 217 | this.wsPublic.on('message', async (raw) => { 218 | try { 219 | const parsed = JSON.parse(raw); 220 | 221 | if (parsed.stream === `markPrice.${this.symbol}`) { 222 | await this.forceClose(this.symbol) 223 | } 224 | } catch (err) { 225 | console.error('❌ Erro no WebSocket público:', err.message); 226 | } 227 | }); 228 | 229 | this.wsPublic.on('close', () => { 230 | console.warn('🔌 Public WebSocket disconnected. Reconnecting...'); 231 | setTimeout(() => this.connectPublic(), 3000); 232 | }); 233 | 234 | this.wsPublic.on('error', (err) => { 235 | console.error('❌ Error on public WebSocket:', err.message); 236 | }); 237 | } 238 | 239 | } 240 | 241 | export default new Grid(); 242 | 243 | 244 | -------------------------------------------------------------------------------- /src/TrailingStop/StopEvaluator.js: -------------------------------------------------------------------------------- 1 | 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | export default class StopEvaluator { 5 | 6 | constructor({ symbol, markPrice, orders, position, account }) { 7 | this.symbol = symbol; 8 | this.markPrice = markPrice; 9 | this.orders = orders || []; 10 | this.position = position; 11 | this.account = account; 12 | this.TRAILING_STOP_GAP = Number(process.env.TRAILING_STOP_GAP) 13 | this.VOLUME_ORDER = Number(process.env.VOLUME_ORDER); 14 | this.MAX_PERCENT_LOSS = Number(String(process.env.MAX_PERCENT_LOSS).replace("%","")) / 100 15 | this.MAX_PERCENT_PROFIT = Number(String(process.env.MAX_PERCENT_PROFIT).replace("%","")) / 100 16 | this.toDelete = [] 17 | this.toCreate = [] 18 | this.toForce = [] 19 | } 20 | 21 | getMarkPriceFromPnl({ entryPrice, qty, totalFee, isLong, targetPnl }) { 22 | const q = Math.abs(qty); 23 | const pnlPlusFee = targetPnl + totalFee; 24 | if (isLong) { 25 | return pnlPlusFee / q + entryPrice; 26 | } else { 27 | return entryPrice - pnlPlusFee / q; 28 | } 29 | } 30 | 31 | filterOrders(orders, marketPrice) { 32 | const above = []; 33 | const below = []; 34 | 35 | for (const order of orders) { 36 | const price = parseFloat(order.price); 37 | if (price > marketPrice) { 38 | above.push(order); 39 | } else if (price < marketPrice) { 40 | below.push(order); 41 | } 42 | } 43 | 44 | // Sort above: farthest to nearest (descending) 45 | above.sort((a, b) => parseFloat(b.price) - parseFloat(a.price)); 46 | 47 | // Sort below: nearest to farthest (descending, so first is closest) 48 | below.sort((a, b) => parseFloat(b.price) - parseFloat(a.price)); 49 | 50 | // Keep 2 farthest above and 1 closest below 51 | const keptOrders = [ 52 | ...above.slice(0, 2), 53 | ...below.slice(0, 1) 54 | ]; 55 | 56 | // Filter out the rest 57 | const removedOrders = orders.filter(order => !keptOrders.includes(order)); 58 | 59 | return removedOrders; 60 | } 61 | 62 | evaluate() { 63 | 64 | const { symbol, markPrice, orders, position, account } = this; 65 | 66 | if (!position || position.qty === 0) return { 67 | toCreate: this.toCreate, 68 | toDelete: this.toDelete, 69 | toForce: this.toForce 70 | } 71 | 72 | const { entryPrice, qty, isLong, notional } = position; 73 | const {makerFee, takerFee} = account 74 | const market = account.markets.find((el) => {return el.symbol === symbol}) 75 | 76 | const feeOpen = (entryPrice * Math.abs(qty)) * makerFee 77 | const feeClose = (markPrice * Math.abs(qty)) * takerFee 78 | const totalFee = feeOpen + feeClose 79 | 80 | const pnl = (isLong ? (markPrice - entryPrice) : (entryPrice - markPrice)) * Math.abs(qty) - totalFee; 81 | 82 | const loss = (notional * this.MAX_PERCENT_LOSS) 83 | const profit = (notional * this.MAX_PERCENT_PROFIT) 84 | 85 | const {decimal_price, decimal_quantity, stepSize_quantity} = market 86 | 87 | const formatPrice = (value) => parseFloat(value).toFixed(decimal_price).toString(); 88 | const formatQuantity = (value) => parseFloat(value).toFixed(decimal_quantity).toString(); 89 | const quantity = formatQuantity(Math.floor(Math.abs(qty) / stepSize_quantity) * stepSize_quantity); 90 | 91 | const price = formatPrice(markPrice) 92 | 93 | const [current_stop] = orders.sort((a, b) => isLong ? Number(a.price) - Number(b.price) : Number(b.price) - Number(a.price)) 94 | 95 | const markePrice_loss = formatPrice(this.getMarkPriceFromPnl({ 96 | entryPrice, 97 | qty, 98 | totalFee, 99 | isLong, 100 | targetPnl: (loss * -1) 101 | })); 102 | 103 | const markePrice_profit = formatPrice(this.getMarkPriceFromPnl({ 104 | entryPrice, 105 | qty, 106 | totalFee, 107 | isLong, 108 | targetPnl: profit 109 | })); 110 | 111 | const breakeven = formatPrice(this.getMarkPriceFromPnl({ 112 | entryPrice, 113 | qty, 114 | totalFee, 115 | isLong, 116 | targetPnl: 0 117 | })); 118 | 119 | const newStop = formatPrice(this.getMarkPriceFromPnl({ 120 | entryPrice, 121 | qty, 122 | totalFee, 123 | isLong, 124 | targetPnl: pnl - this.TRAILING_STOP_GAP 125 | })); 126 | 127 | const limit_minimal_vol = notional < (this.VOLUME_ORDER * 0.2) 128 | const max_profit = isLong ? price > markePrice_profit : price < markePrice_profit 129 | const max_loss = isLong ? price < markePrice_loss : price > markePrice_loss 130 | 131 | 132 | if(max_profit || max_loss || limit_minimal_vol){ 133 | this.toForce.push({ 134 | symbol:symbol 135 | }) 136 | } else { 137 | 138 | if(orders.length === 0) { 139 | 140 | this.toCreate.push({ 141 | symbol, 142 | price: formatPrice(markePrice_profit), 143 | side: isLong ? 'Ask' : 'Bid', 144 | quantity: quantity 145 | }) 146 | 147 | this.toCreate.push({ 148 | symbol, 149 | price:formatPrice(markePrice_loss), 150 | side: isLong ? 'Ask' : 'Bid', 151 | quantity: quantity 152 | }) 153 | 154 | } else if(orders.length === 1) { 155 | const current_price = Number(current_stop.price) 156 | if(current_price > markPrice && isLong || current_price < markPrice && !isLong) { 157 | // is profit, add stop 158 | this.toCreate.push({ 159 | symbol, 160 | price:formatPrice(markePrice_loss), 161 | side: isLong ? 'Ask' : 'Bid', 162 | quantity: quantity 163 | }) 164 | } else { 165 | // is stop, add profit 166 | this.toCreate.push({ 167 | symbol, 168 | price: formatPrice(markePrice_profit), 169 | side: isLong ? 'Ask' : 'Bid', 170 | quantity: quantity 171 | }) 172 | } 173 | 174 | } else if(orders.length === 2) { 175 | const current_price = formatPrice(current_stop.price) 176 | const token_gap = this.TRAILING_STOP_GAP / quantity 177 | const suggestion_stop = formatPrice(isLong ? markPrice - token_gap : current_price + token_gap) 178 | 179 | const pnlGap = pnl - this.TRAILING_STOP_GAP 180 | const updateIsValid = isLong ? 181 | (current_price < suggestion_stop && suggestion_stop < markPrice) : 182 | (current_price > suggestion_stop && suggestion_stop > markPrice) 183 | 184 | if(pnlGap > 0) { 185 | 186 | if(updateIsValid) { 187 | 188 | this.toDelete.push({ 189 | id:current_stop.id, 190 | symbol:current_stop.symbol 191 | }) 192 | 193 | this.toCreate.push({ 194 | symbol, 195 | price: suggestion_stop, 196 | side: isLong ? 'Ask' : 'Bid', 197 | quantity: quantity 198 | }) 199 | 200 | } 201 | 202 | } 203 | 204 | 205 | } else if(orders.length > 2) { 206 | const removes = this.filterOrders(orders) 207 | for (const remove of removes) { 208 | this.toDelete.push({ 209 | id:remove.id, 210 | symbol:remove.symbol 211 | }) 212 | } 213 | } 214 | 215 | } 216 | 217 | 218 | return { 219 | toCreate: this.toCreate, 220 | toDelete: this.toDelete, 221 | toForce: this.toForce 222 | } 223 | } 224 | } 225 | 226 | -------------------------------------------------------------------------------- /src/TrailingStop/TrailingStopStream.js: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws'; 2 | import dotenv from 'dotenv'; 3 | import { auth } from '../Backpack/Authenticated/Authentication.js'; 4 | import OrderController from '../Controllers/OrderController.js'; 5 | import CacheController from '../Controllers/CacheController.js'; 6 | import StopEvaluator from './StopEvaluator.js'; 7 | import Order from '../Backpack/Authenticated/Order.js'; 8 | import Futures from '../Backpack/Authenticated/Futures.js'; 9 | const Cache = new CacheController(); 10 | dotenv.config(); 11 | 12 | class TrailingStopStream { 13 | 14 | constructor() { 15 | this.VOLUME_ORDER = Number(process.env.VOLUME_ORDER); 16 | this.MAX_PERCENT_LOSS = Number(String(process.env.MAX_PERCENT_LOSS).replace("%", "")); 17 | this.wsPrivate = null; 18 | this.wsPublic = null; 19 | this.positions = {}; 20 | this.activeStops = {}; 21 | this.subscribedSymbols = new Set(); 22 | } 23 | 24 | async updatePositions() { 25 | const positions = await Futures.getOpenPositions() 26 | if(positions){ 27 | this.positions = {} 28 | const account = await Cache.get(); 29 | 30 | for (const position of positions) { 31 | const symbol = position.symbol 32 | const markPrice = Number(position.markPrice); 33 | const entryPrice = Number(position.entryPrice); 34 | const notional = Number(position.netExposureNotional) 35 | const fee = Math.abs(notional * account.fee) * 2; 36 | const pnlRealized = Number(position.pnlRealized); 37 | const pnlUnrealized = Number(position.pnlUnrealized); 38 | const pnl = (pnlRealized + pnlUnrealized) - fee; 39 | const qty = Number(position.netExposureQuantity) 40 | const isLong = parseFloat(position.netQuantity) > 0; 41 | 42 | this.positions[symbol] = { 43 | symbol, 44 | markPrice, 45 | entryPrice, 46 | pnl, 47 | qty, 48 | notional, 49 | isLong 50 | }; 51 | } 52 | 53 | this.syncMarkPriceSubscriptions(); 54 | } 55 | } 56 | 57 | async updateStops(symbol) { 58 | const orders = await Order.getOpenOrders(symbol) 59 | this.activeStops[symbol] = orders; 60 | } 61 | 62 | async onPositionUpdate(data) { 63 | const symbol = data.s; 64 | const account = await Cache.get(); 65 | 66 | const markPrice = Number(data.M); 67 | const entryPrice = Number(data.B); 68 | const pnlRealized = Number(data.p); 69 | const pnlUnrealized = Number(data.P); 70 | const notional = Number(data.n); 71 | const qty = Number(data.q); 72 | const isLong = qty > 0 73 | 74 | const fee = Math.abs(notional * account.fee) * 2; 75 | const pnl = (pnlRealized + pnlUnrealized) - fee; 76 | 77 | this.positions[symbol] = { 78 | symbol, 79 | markPrice, 80 | entryPrice, 81 | pnl, 82 | qty, 83 | notional, 84 | isLong 85 | }; 86 | 87 | await this.updateStops(symbol) 88 | this.syncMarkPriceSubscriptions(); 89 | 90 | } 91 | 92 | async onMarkPriceUpdate(symbol, markPrice) { 93 | 94 | const position = this.positions[symbol]; 95 | 96 | if(position){ 97 | const orders = await Order.getOpenOrders(symbol) 98 | const account = await Cache.get() 99 | 100 | const evaluator = new StopEvaluator({ 101 | symbol, 102 | markPrice, 103 | orders, 104 | position, 105 | account 106 | }); 107 | 108 | const {toCreate, toDelete, toForce} = evaluator.evaluate(); 109 | 110 | for (const row of toForce) { 111 | const positions = await Futures.getOpenPositions() 112 | const position = positions.find((el) => {return el.symbol === row.symbol}) 113 | if(position){ 114 | await OrderController.forceClose(position) 115 | } 116 | } 117 | 118 | for (const row of toDelete) { 119 | await Order.cancelOpenOrder(row.symbol, row.id) 120 | } 121 | 122 | for (const row of toCreate) { 123 | await OrderController.createLimitTriggerStop(row.symbol, row.side, row.price, row.quantity, account, markPrice); 124 | } 125 | 126 | } 127 | } 128 | 129 | syncMarkPriceSubscriptions() { 130 | if (!this.wsPublic || this.wsPublic.readyState !== WebSocket.OPEN) return; 131 | 132 | const openSymbols = Object.keys(this.positions); // símbolos com posições abertas 133 | 134 | if (openSymbols.length === 0) return; 135 | 136 | // Verifica quais símbolos devem ser desinscritos 137 | const symbolsToUnsubscribe = [...this.subscribedSymbols].filter(symbol => !openSymbols.includes(symbol)); 138 | const symbolsToSubscribe = openSymbols.filter(symbol => !this.subscribedSymbols.has(symbol)); 139 | 140 | // Atualiza o Set com os novos símbolos válidos 141 | for (const symbol of symbolsToUnsubscribe) { 142 | this.subscribedSymbols.delete(symbol); 143 | } 144 | 145 | for (const symbol of symbolsToSubscribe) { 146 | this.subscribedSymbols.add(symbol); 147 | } 148 | 149 | // Envia UNSUBSCRIBE para símbolos inválidos 150 | if (symbolsToUnsubscribe.length > 0) { 151 | const payload = { 152 | method: 'UNSUBSCRIBE', 153 | params: symbolsToUnsubscribe.map(s => `markPrice.${s}`) 154 | }; 155 | this.wsPublic.send(JSON.stringify(payload)); 156 | } 157 | 158 | // Envia SUBSCRIBE para novos símbolos 159 | if (symbolsToSubscribe.length > 0) { 160 | const payload = { 161 | method: 'SUBSCRIBE', 162 | params: symbolsToSubscribe.map(s => `markPrice.${s}`) 163 | }; 164 | this.wsPublic.send(JSON.stringify(payload)); 165 | } 166 | } 167 | 168 | connectPrivate() { 169 | this.wsPrivate = new WebSocket('wss://ws.backpack.exchange'); 170 | 171 | this.wsPrivate.on('open', () => { 172 | console.log('✅ WebSocket privado conectado'); 173 | 174 | const timestamp = Date.now(); 175 | const window = 10000; 176 | const instruction = 'subscribe'; 177 | const params = {}; 178 | const headers = auth({ instruction, params, timestamp, window }); 179 | 180 | const payload = { 181 | method: 'SUBSCRIBE', 182 | params: ['account.positionUpdate', 'account.orderUpdate'], 183 | signature: [ 184 | headers['X-API-Key'], 185 | headers['X-Signature'], 186 | headers['X-Timestamp'], 187 | headers['X-Window'] 188 | ] 189 | }; 190 | 191 | this.wsPrivate.send(JSON.stringify(payload)); 192 | }); 193 | 194 | this.wsPrivate.on('message', async (raw) => { 195 | try { 196 | 197 | const parsed = JSON.parse(raw); 198 | 199 | if (parsed.stream === 'account.positionUpdate') { 200 | await this.onPositionUpdate(parsed.data); 201 | } 202 | 203 | if (parsed.stream === 'account.orderUpdate') { 204 | 205 | if (["orderCancelled", "orderExpired", "triggerFailed"].includes(parsed.data.e)){ 206 | await this.updatePositions() 207 | await this.updateStops(parsed.data.s) 208 | } 209 | 210 | if(["orderFill", "orderAccepted", "triggerPlaced", "orderAccepted"].includes(parsed.data.e)){ 211 | await this.updatePositions() 212 | } 213 | } 214 | 215 | } catch (err) { 216 | console.error('❌ Erro ao processar posição:', err); 217 | } 218 | }); 219 | 220 | this.wsPrivate.on('close', () => { 221 | console.log('🔌 WebSocket privado fechado. Reconectando...'); 222 | reconectPrivate() 223 | }); 224 | 225 | this.wsPrivate.on('error', (err) => { 226 | console.error('❌ Erro no WebSocket privado:', err); 227 | reconectPrivate() 228 | }); 229 | } 230 | 231 | connectPublic() { 232 | this.wsPublic = new WebSocket('wss://ws.backpack.exchange'); 233 | 234 | this.wsPublic.on('open', () => { 235 | console.log('✅ WebSocket público conectado'); 236 | this.syncMarkPriceSubscriptions(); 237 | }); 238 | 239 | this.wsPublic.on('message', async (raw) => { 240 | try { 241 | const parsed = JSON.parse(raw); 242 | const match = parsed.stream?.match(/^markPrice\.(.+)$/); 243 | if (match) { 244 | const symbol = match[1]; 245 | const markPrice = Number(parsed.data.p); 246 | await this.onMarkPriceUpdate(symbol, markPrice); 247 | } 248 | } catch (err) { 249 | console.error('❌ Erro no markPrice:', err); 250 | } 251 | }); 252 | 253 | this.wsPublic.on('close', () => { 254 | console.log('🔌 WebSocket público fechado. Reconectando...'); 255 | reconectPublic() 256 | }); 257 | 258 | this.wsPublic.on('error', (err) => { 259 | console.error('❌ Erro no WebSocket público:', err); 260 | reconectPublic() 261 | }); 262 | } 263 | 264 | reconectPrivate() { 265 | this.wsPrivate?.terminate(); 266 | this.wsPrivate = null; 267 | setTimeout(() => this.connectPrivate(), 3000); 268 | } 269 | 270 | reconectPublic() { 271 | this.wsPublic?.terminate(); 272 | this.wsPublic = null; 273 | setTimeout(() => this.connectPublic(), 3000); 274 | } 275 | 276 | start() { 277 | this.connectPrivate(); 278 | this.connectPublic(); 279 | 280 | setInterval(() => { 281 | if (!this.wsPrivate || this.wsPrivate.readyState !== WebSocket.OPEN) { 282 | console.warn('⚠️ wsPrivate inativo. Reconectando...'); 283 | this.connectPrivate(); 284 | } 285 | 286 | if (!this.wsPublic || this.wsPublic.readyState !== WebSocket.OPEN) { 287 | console.warn('⚠️ wsPublic inativo. Reconectando...'); 288 | this.connectPublic(); 289 | } 290 | }, 30_000); 291 | } 292 | 293 | } 294 | 295 | export default new TrailingStopStream(); 296 | -------------------------------------------------------------------------------- /src/Utils/Terminal.js: -------------------------------------------------------------------------------- 1 | class Terminal { 2 | 3 | init(total = 100, width = 30) { 4 | this.total = total; 5 | this.width = width; 6 | this.current = 0; 7 | this.titl 8 | } 9 | 10 | update(title, value) { 11 | this.current = Math.min(value, this.total); 12 | const percent = this.current / this.total; 13 | const filledLength = Math.round(this.width * percent); 14 | const bar = '█'.repeat(filledLength) + '-'.repeat(this.width - filledLength); 15 | const percentText = (percent * 100).toFixed(1).padStart(5, ' '); 16 | 17 | process.stdout.clearLine(); 18 | process.stdout.cursorTo(0); 19 | process.stdout.write(`${title} [${bar}] ${percentText}% `); 20 | } 21 | 22 | finish() { 23 | process.stdout.write('\n'); 24 | } 25 | } 26 | 27 | export default new Terminal(); -------------------------------------------------------------------------------- /src/Utils/Utils.js: -------------------------------------------------------------------------------- 1 | class Utils { 2 | minutesAgo(timestampMs) { 3 | const now = Date.now(); 4 | const diffMs = now - timestampMs; 5 | return Math.floor(diffMs / 60000); 6 | } 7 | 8 | getIntervalInSeconds(interval) { 9 | if (typeof interval !== 'string') return 60; 10 | 11 | const match = interval.match(/^(\d+)([smhd])$/i); 12 | if (!match) return 60; 13 | 14 | const value = parseInt(match[1], 10); 15 | const unit = match[2].toLowerCase(); 16 | 17 | const unitToSeconds = { 18 | s: 1, 19 | m: 60, 20 | h: 3600, 21 | d: 86400, 22 | }; 23 | 24 | return value * (unitToSeconds[unit] || 60); 25 | } 26 | } 27 | 28 | export default new Utils(); 29 | --------------------------------------------------------------------------------