├── account_cache.db ├── .gitignore ├── package.json ├── src ├── Utils │ ├── Utils.js │ └── Terminal.js ├── Backpack │ ├── Authenticated │ │ ├── Futures.js │ │ ├── Authentication.js │ │ ├── BorrowLend.js │ │ ├── Capital.js │ │ ├── Account.js │ │ ├── Order.js │ │ └── History.js │ └── Public │ │ ├── Assets.js │ │ ├── BorrowLend.js │ │ ├── Trades.js │ │ ├── System.js │ │ └── Markets.js ├── Controllers │ ├── CacheController.js │ ├── AccountController.js │ ├── PnlController.js │ └── OrderController.js ├── Achievements │ └── Achievements.js ├── Decision │ ├── Decision.js │ └── Indicators.js ├── TrailingStop │ ├── StopEvaluator.js │ └── TrailingStopStream.js └── Grid │ └── Grid.js ├── .env ├── LICENSE ├── app.js └── README.md /account_cache.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heron-jr/backbot/HEAD/account_cache.db -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | --------------------------------------------------------------------------------