├── .eslintrc.js ├── strategies ├── reverseMomentumTrading │ ├── reverseMomentumTrading.md │ ├── reverseMomentumTradingAnalyzer.js │ └── reverseMomentumTrading.js ├── momentumTrading │ ├── momentumTrading.md │ ├── momentumTradingAnalyzer.js │ └── momentumTrading.js └── momentumTradingWithStopLoss │ ├── momentumTradingWithStopLoss.md │ └── momentumTradingWithStopLoss.js ├── package.json ├── index.js ├── .gitignore ├── README.md ├── coinbaseProLibrary.js └── buyAndSell.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 12 10 | }, 11 | "rules": { 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /strategies/reverseMomentumTrading/reverseMomentumTrading.md: -------------------------------------------------------------------------------- 1 | # Momentum Trading Strategy 2 | 3 | ## Summary: 4 | This trading strategy works specifying a buy and sell delta. The buy delta is how far the price has to drop before it buys in. The sell price is how far the price has to rise before the bot sells out. This is the reverse of the momentum trading strategy. -------------------------------------------------------------------------------- /strategies/momentumTrading/momentumTrading.md: -------------------------------------------------------------------------------- 1 | # Momentum Trading Strategy 2 | 3 | ## Summary: 4 | This trading strategy works specifying a buy and sell delta. When the bot starts it waits for the buy delta condition to be reached. Say for example the buy delta is 2%, once the price rises to meet that delta then the bot will trigger a buy in. The idea behind momentum trading is that when the price goes up there's momentum that will continue to rise up. The Sell delta price is the opposite of the buy, where when the price drops by that amount it will sell out. 5 | -------------------------------------------------------------------------------- /strategies/momentumTradingWithStopLoss/momentumTradingWithStopLoss.md: -------------------------------------------------------------------------------- 1 | # Momentum Trading Strategy 2 | 3 | ## Summary: 4 | This trading strategy works specifying a buy and sell delta. When the bot starts it waits for the buy delta condition to be reached. Say for example the buy delta is 2%, once the price rises to meet that delta then the bot will trigger a buy in. The idea behind momentum trading is that when the price goes up there's momentum that will continue to rise up. The Sell delta price is the opposite of the buy, where when the price drops by that amount it will sell out. 5 | Additionally this strategy will automatically sell and reset if the price falls below a set percentage of the original buy. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cryppledbot", 3 | "version": "1.0.0", 4 | "description": "This is a crypto trading bot implemented in nodejs that utilizes the coinbase pro API. It's trading strategy is a basic interpretation of the momemtum strategy.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/poptartboy0/CryppledBot.git" 13 | }, 14 | "keywords": [ 15 | "crypto", 16 | "bot", 17 | "coinbase-pro", 18 | "btc" 19 | ], 20 | "author": "Levi Leuthold", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/poptartboy0/CryppledBot/issues" 24 | }, 25 | "homepage": "https://github.com/poptartboy0/CryppledBot#readme", 26 | "dependencies": { 27 | "axios": "^0.21.1", 28 | "coinbase-pro": "^0.9.0", 29 | "csv-parse": "^4.15.4", 30 | "csv-parser": "^2.3.3", 31 | "dotenv": "^8.6.0", 32 | "node-csv": "^0.1.2", 33 | "pino": "^6.11.3" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^7.27.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | /* 4 | * This is the entry point of program. Select the strategy or analyzer(s) 5 | */ 6 | const momentumStrategyStart = require("./strategies/momentumTrading/momentumTrading"); 7 | const momentumStrategyAnalyzerStart = require("./strategies/momentumTrading/momentumTradingAnalyzer"); 8 | 9 | const momentumWithStopLossStrategyStart = require("./strategies/momentumTradingWithStopLoss/momentumTradingWithStopLoss"); 10 | 11 | const reverseMomentumStrategyStart = require("./strategies/reverseMomentumTrading/reverseMomentumTrading"); 12 | const reverseMomentumStrategyAnalyzerStart = require("./strategies/reverseMomentumTrading/reverseMomentumTradingAnalyzer"); 13 | 14 | 15 | /*** Make sure to configure the momentumStrategy in ./strategies/momentumTrading/momentumTrading.js or in the .env before launching ***/ 16 | //Launches the momentum strategy and starts the bot: 17 | momentumStrategyStart(); 18 | 19 | //Launches the momentum strategy anaylzer for back testing: 20 | //momentumStrategyAnalyzerStart(); 21 | 22 | // ********************************************************************************************************************** 23 | 24 | /*** Make sure to configure the momentumStrategy in ./strategies/momentumTrading/momentumTrading.js or in the .env before launching ***/ 25 | //Launches the reverse momentum strategy and starts the bot: 26 | //reverseMomentumStrategyStart(); 27 | 28 | //Launches the reverse momentum strategy anaylzer for back testing: 29 | //reverseMomentumStrategyAnalyzerStart(); 30 | 31 | // ********************************************************************************************************************** 32 | 33 | /*** Make sure to configure the momentumWithStopLossStrategy in ./strategies/momentumTradingWithStopLoss/momentumTradingWithStopLoss.js or in the .env before launching ***/ 34 | //Launches the momentum with stop loss strategy and starts the bot: 35 | //momentumWithStopLossStrategyStart(); 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Skip to content 2 | github 3 | / 4 | gitignore 5 | Code 6 | Pull requests 7 | 191 8 | Actions 9 | Projects 10 | Security 11 | More 12 | gitignore/Node.gitignore 13 | @ch4ot1c 14 | ch4ot1c Add .yarn/install-state.gz to Node.gitignore (#3407) 15 | … 16 | History 17 | 58 contributors 18 | @stuartpb@shiftkey@SimonSiefke@TennyZhuang@venatoria@ro31337@Richienb@melonmanchan@gouthamve@jucrouzet@cheddar@arcresu 19 | 116 lines (86 sloc) 1.77 KB 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | lerna-debug.log* 28 | 29 | # Diagnostic reports (https://nodejs.org/api/report.html) 30 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Directory for instrumented libs generated by jscoverage/JSCover 39 | lib-cov 40 | 41 | # Coverage directory used by tools like istanbul 42 | coverage 43 | *.lcov 44 | 45 | # nyc test coverage 46 | .nyc_output 47 | 48 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # Bower dependency directory (https://bower.io/) 52 | bower_components 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (https://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # Snowpack dependency directory (https://snowpack.dev/) 65 | web_modules/ 66 | 67 | # TypeScript cache 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | .npm 72 | 73 | # Optional eslint cache 74 | .eslintcache 75 | 76 | # Microbundle cache 77 | .rpt2_cache/ 78 | .rts2_cache_cjs/ 79 | .rts2_cache_es/ 80 | .rts2_cache_umd/ 81 | 82 | # Optional REPL history 83 | .node_repl_history 84 | 85 | # Output of 'npm pack' 86 | *.tgz 87 | 88 | # Yarn Integrity file 89 | .yarn-integrity 90 | 91 | # dotenv environment variables file 92 | .env 93 | .env.test 94 | 95 | # parcel-bundler cache (https://parceljs.org/) 96 | .cache 97 | .parcel-cache 98 | 99 | # Next.js build output 100 | .next 101 | out 102 | 103 | # Nuxt.js build / generate output 104 | .nuxt 105 | dist 106 | 107 | # Gatsby files 108 | .cache/ 109 | # Comment in the public line in if your project uses Gatsby and not Next.js 110 | # https://nextjs.org/blog/next-9-1#public-directory-support 111 | # public 112 | 113 | # vuepress build output 114 | .vuepress/dist 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | © 2020 GitHub, Inc. 138 | Terms 139 | Privacy 140 | Security 141 | Status 142 | Help 143 | Contact GitHub 144 | Pricing 145 | API 146 | Training 147 | Blog 148 | About 149 | 150 | # Cryp Finder 151 | positionData.json 152 | *.csv -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CrypFinder Bot 2 | ## Version 1.55 3 | 4 | ## Update: 5 | I made a simplified version of this bot for BinanceUS using Python also on my profile. I don't really work on this project anymore but it can serve as a starting point very basic crypto trading bot 6 | 7 | ## Summary: 8 | CrypFinder is a Coinbase Pro API trading bot that currently implements a basic momentum trading strategy and reverse momentum trading strategy in NodeJS using the Coinbase Pro API, as well as its own custom library for the endpoints that are not supported by the now deprecated Coinbase Pro NodeJS Library. Currently, Coinbase Pro limits the number of portfolios to five, this means that the bot can run up to four trading instances simultaneously per Coinbase Pro account. This bot can be modified to trade any product pairs available on Coinbase Pro, such as BTC-USD, ETH-USD, etc., but stablecoin (USDC to other coins) and crypto markets (coin to other coins) aren't currently tested, only USD markets (USD to coins). 9 | 10 | The momentum strategy will work as follows: The bot will start by getting the amount of USD available for the provided API key's profile (profile=portfolio on the coinbase pro website). If the amount is greater than zero, it will monitor the price changes of the chosen product using a peak/valley system; if the price changes by the specified delta, it will purchase a position. Then, it will monitor price changes until the delta condition and profit condition are met; after selling for a profit, it can deposit a cut of the profit to a different portfolio for saving. The reverse momentum trading strategy, is, as the name implies the reverse where it sells when the price goes up and buys when it goes down. 11 | 12 | The bot features a number of variables at the top that can be configured to customize how it trades. This includes the deltas that specify an amount of price change that will trigger a buy/sell, the minimum acceptable profit from a trade, the name of currency to trade, the profile names, the deposit enable/disable flag, the deposit amount, and more. Of course, any of the code can be modified to customize it fully. This project is a great way to trade crypto on Coinbase Pro through their API. 13 | 14 | ## How to run the program: 15 | I suggest starting by using this program in the [Coinbase Pro Sandbox](https://docs.pro.coinbase.com/#sandbox) for testing. 16 | 1. Create a coinbase pro account. Install NodeJS, install Git, consider adding an ESLint plugin to your IDE to use the eslint configuration. 17 | 2. Setup your Coinbase Pro account portfolios (portfolios are also referred to as profiles). Each bot that runs needs it's own portfolio. The bot will take any available balance in the portfolio that it's tied to via the API key and start trading with it. Coinbase Pro gives you the default (Default Portfolio) to start with, but, you can add up to four more (5 is the max). Don't use a bot to trade in the default portfolio because that's where money transfers go by default; which, means the funds could get swept up by the bot before you can allocate them where you want. Create a new portfolio for each bot you want to run, for example you could create one called "BTC trader" that the bot trades bitcoin inside of. If you wish to use the feature that deposits all or a portion of profits into another portfolio to save it then by default those deposits will go to the default portfolio, but you have the option to create a different portfolio to use instead. Take note of the profile names you created as you will need them in steps 5 & 6. 18 | 3. Create the API key for the portfolio you want the bot to trade on, give it View/Trade/Transfer permissions and whitelist your public IP. [More info here](https://help.coinbase.com/en/pro/other-topics/api/how-do-i-create-an-api-key-for-coinbase-pro). 19 | 4. Clone the github repo locally and run `npm install` from within the repo directory. 20 | 5. Configure the variables at the top of ../Strategies/MomentumTrading/momentumTrading.js to select your Deltas, product to trade,Profile Names etc. DO NOT ADD YOUR API INFORMATION HERE. You will use your API information in Step 6. The variables can also be added to the .env file instead of directly edited in code. 21 | 6. Create a .env file in the root directory of the projects repo with the following: 22 | 23 | API_KEY=\ 24 | 25 | API_SECRET=\ 26 | 27 | API_PASSPHRASE=\ 28 | 29 | TRADING_ENV=\ Leaving this out defaults to sandbox environment 30 | 31 | Additionally consider adding `LOG_LEVEL=debug` here if you want the full debug logs. 32 | 33 | All of the the trading variables can be configured in the code or in the .env below is a list of these variables: 34 | 35 | SELL_POSITION_DELTA=\ 36 | 37 | BUY_POSITION_DELTA=\ 38 | 39 | ORDER_PRICE_DELTA=\ 40 | 41 | BASE_CURRENCY_NAME=\ 42 | 43 | QUOTE_CURRENCY_NAME=\ 44 | 45 | TRADING_PROFILE_NAME=\ 46 | 47 | DEPOSIT_PROFILE_NAME=\ 48 | 49 | DEPOSITING_ENABLED=\ 50 | 51 | DEPOSITING_AMOUNT=\ 52 | 53 | BALANCE_MINIMUM=\ 54 | 55 | 7. Add some funds to your default portfolio and make sure there is no existing coin balance for the product you're trading if you're just starting the bot. See "Restarting the bot" if you want the bot to pick up where it left off after stopping it. 56 | 8. Run the program with `node index.js` from within the repo directory. 57 | 58 | ### Restarting the bot: 59 | If at any point the bot stops running for any reason, the the bot keeps a file called positionData.json that tracks whether or not it was in a position, and the information associated with that position. If you restart the bot it will read that file and automatically pick up where it left off. Don't try to add more coins to the existing position or it will cause unexpected behavior since the bot won't know the associated costs with the newly added coin. You can, at any point, add USD to a portfolio, the bot will start trading with the newly added USD when it completes a buy/sell cycle. If you want to start the bot fresh without existing data, simply make sure there is no left over coin in your profile and delete the positionData.json file. 60 | 61 | The positionData.json file contains a JSON object with 3 fields, positionExists (boolean), positionAcquiredPrice (Number), positionAcquiredCost (Number). 62 | 63 | Example of a position existing positionData.json file: 64 | {"positionExists":true,"positionAcquiredPrice":48560.00000000001,"positionAcquiredCost":274.66836873840003} 65 | 66 | Example of a position not existing positionData.json file: 67 | {"positionExists":false,"positionAcquiredPrice":228.69000000000003,"positionAcquiredCost":299.85119860983207} 68 | 69 | Notice that the position acquired cost and price fields still exist in the file when positionExists is false, but they are ignored. 70 | 71 | ## How to contribute: 72 | 1. Fork the repo. 73 | 2. Clone the github repo locally and run `npm install` 74 | 3. Create a new branch with a name that describes the feature/change you're making. 75 | 4. Check the road map for ideas of things to work on. 76 | 5. Make your changes and commit them with a descriptive message. 77 | 6. Push your changes upstream. 78 | 7. When you're done testing your changes, create a pull request to merge your repository into LeviathanLevi/Coinbase-Pro-Crypto-Trading-Bot-CrypFinder. Then wait for approval. 79 | 80 | ## Running the program out of sandbox: 81 | When you're confident in the configuration/code base and want to run it in the real environment, comment out the sandbox env variables and uncomment out the real API URI variables. Update the .env file with a valid API key 82 | 83 | ## Momentum and reverse momentum trading strategy analyzer: 84 | The analyzers are a way to run data against the bot strategy to see how well it performs. It takes in a .csv file with OHLC data. Carston Klein has already compiled a massive dataset that is perfect for this task and it's available for free on Kaggle [check it out](https://www.kaggle.com/tencars/392-crypto-currency-pairs-at-minute-resolution?select=ampusd.csv). After downloading the file for the coin data you want, just trim the .csv file to the length of time you want to test and run the analyzer with the configuration you want and it will generate a report showing how it did. He also wrote [this article](https://medium.com/coinmonks/how-to-get-historical-crypto-currency-data-954062d40d2d) on how to get similar data yourself. 85 | 86 | ## Helpful links: 87 | [Coinbase Pro](https://pro.coinbase.com/trade/BTC-USD) 88 | 89 | [Coinbase Pro API Docs](https://docs.pro.coinbase.com/#introduction) 90 | 91 | [Coinbase Pro NodeJS Library](https://www.npmjs.com/package/coinbase-pro) 92 | 93 | [Flow diagram of the momentum strategy, open it in Google draw.io for best results (May be outdated, but can help to give an idea of how the program works)](https://drive.google.com/file/d/1sMg7nWcuCDwHS5wdwHgoe5qqODO7UEFA/view?usp=sharing) 94 | -------------------------------------------------------------------------------- /coinbaseProLibrary.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The official coinbase-pro library (https://www.npmjs.com/package/coinbase-pro) has been deprecated as of January 16th, 2020. 3 | * The coinbase-pro library still works but it doesn't support all of the API endpoints being used by this project. As a work 4 | * around, this file will create a library that supports those other methods needed for this bot to run. 5 | */ 6 | const crypto = require("crypto"); 7 | const axios = require("axios"); 8 | const pino = require("pino"); 9 | const logger = pino({ level: process.env.LOG_LEVEL || "info" }); 10 | 11 | const numberOfAttempts = 3; 12 | 13 | /** 14 | * Class: This class creates an easy way to create methods to call API endpoints. It stores 15 | * the needed information upon construction and provides a method to sign messages. Which 16 | * can then be used to create more endpoint calling methods. 17 | */ 18 | class coinbaseProLib { 19 | /** 20 | * Summary: constructs an instance of this class that can be used to make the API endpoint calls 21 | * 22 | * @param {string} apiKey 23 | * @param {string} apiSecret 24 | * @param {string} apiPassphrase 25 | * @param {string} apiURI 26 | */ 27 | constructor(apiKey, apiSecret, apiPassphrase, apiURI) { 28 | this.apiKey = apiKey; 29 | this.apiSecret = apiSecret; 30 | this.apiPassphrase = apiPassphrase; 31 | this.apiURI = apiURI; 32 | } 33 | 34 | /** 35 | * Creates the CB-ACCESS-SIGN header needed for executing a coinbase pro REST API endpoint call. 36 | * 37 | * @param {string} method 38 | * @param {string} requestPath 39 | * @param {string} body 40 | * 41 | * @return {string} CB-ACCESS-SIGN value 42 | */ 43 | async signMessage(method, requestPath, body) { 44 | try { 45 | if (method == null || requestPath == null) { 46 | throw new Error("Error in signMessage method, method or requestPath is null!"); 47 | } 48 | 49 | const timestamp = Date.now() / 1000; 50 | 51 | let what; 52 | 53 | if (body == null) { 54 | what = timestamp + method + requestPath; 55 | } else { 56 | what = timestamp + method + requestPath + JSON.stringify(body); 57 | } 58 | 59 | // decode the base64 secret 60 | const key = Buffer.from(this.apiSecret, 'base64'); 61 | 62 | // create a sha256 hmac with the secret 63 | const hmac = crypto.createHmac('sha256', key); 64 | 65 | // sign the require message with the hmac 66 | // and finally base64 encode the result 67 | const result = hmac.update(what).digest('base64'); 68 | 69 | return result; 70 | } catch (err) { 71 | const message = "Error occurred in signMessage method."; 72 | const errorMsg = new Error(err); 73 | logger.error({ message, errorMsg, err }); 74 | throw err; 75 | } 76 | } 77 | 78 | /** 79 | * Calls the endpoint /profiles to get a list of the available portfolio (profile) IDs for the account 80 | * Check the documentation for more information on this endpoint. 81 | * 82 | * @return {string} API call response data 83 | */ 84 | async getProfiles() { 85 | let attempts = 0; 86 | 87 | while (attempts < numberOfAttempts) { 88 | try { 89 | const method = "GET"; 90 | const requestPath = "/profiles"; 91 | const body = null; 92 | const timestamp = Date.now() / 1000; 93 | 94 | const sign = await this.signMessage(method, requestPath, body); 95 | 96 | const headers = { 97 | "CB-ACCESS-KEY": this.apiKey, 98 | "CB-ACCESS-SIGN": sign, 99 | "CB-ACCESS-TIMESTAMP": timestamp, 100 | "CB-ACCESS-PASSPHRASE": this.apiPassphrase 101 | }; 102 | 103 | const fullpath = this.apiURI + requestPath; 104 | 105 | //Call API: 106 | const result = await axios.get(fullpath, { headers }); 107 | 108 | return result.data; 109 | } catch (err) { 110 | if (attempts < numberOfAttempts - 1) { 111 | attempts++; 112 | } else { 113 | const message = "Error occurred in getProfiles method. Number of attempts: " + numberOfAttempts; 114 | const errorMsg = new Error(err); 115 | logger.error({ message, errorMsg, err }); 116 | throw err; 117 | } 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Calls the endpoint /fees to get the current maker and taker fees 124 | * Check the documentation for more information on this endpoint. 125 | * 126 | * Re-attempts: 3 127 | * 128 | * @return {array} API call response data 129 | */ 130 | async getFees() { 131 | let attempts = 0; 132 | 133 | while (attempts < numberOfAttempts) { 134 | try { 135 | const method = "GET"; 136 | const requestPath = "/fees"; 137 | const body = null; 138 | const timestamp = Date.now() / 1000; 139 | 140 | const sign = await this.signMessage(method, requestPath, body); 141 | 142 | const headers = { 143 | "CB-ACCESS-KEY": this.apiKey, 144 | "CB-ACCESS-SIGN": sign, 145 | "CB-ACCESS-TIMESTAMP": timestamp, 146 | "CB-ACCESS-PASSPHRASE": this.apiPassphrase 147 | }; 148 | 149 | const fullpath = this.apiURI + requestPath; 150 | 151 | //Call API: 152 | const result = await axios.get(fullpath, { headers }); 153 | 154 | return result.data; 155 | } catch (err) { 156 | if (attempts < numberOfAttempts - 1) { 157 | attempts++; 158 | } else { 159 | const message = "Error occurred in getFees method. Number of attempts: " + numberOfAttempts; 160 | const errorMsg = new Error(err); 161 | logger.error({ message, errorMsg, err }); 162 | throw err; 163 | } 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * Calls the /profiles/transfer endpoint that will let you transfer some currency from one profile to another. 170 | * The fromProfileID must be the profile linked to the API key provided, this is where the funds are sourced. 171 | * Check the coinbase pro api docs for more information on the restrictions around this endpoint. 172 | * 173 | * @param {string} fromProfileID 174 | * @param {string} toProfileID 175 | * @param {string} currency 176 | * @param {string} amount 177 | * 178 | * Re-attempts: 3 179 | * 180 | * @return {string} data from the response 181 | */ 182 | async profileTransfer(fromProfileID, toProfileID, currency, amount) { 183 | let attempts = 0; 184 | 185 | while (attempts < numberOfAttempts) { 186 | try { 187 | const method = "POST"; 188 | const requestPath = "/profiles/transfer"; 189 | const body = { 190 | from: fromProfileID, 191 | to: toProfileID, 192 | currency, 193 | amount, 194 | }; 195 | 196 | const timestamp = Date.now() / 1000; 197 | 198 | const sign = await this.signMessage(method, requestPath, body); 199 | 200 | const headers = { 201 | "CB-ACCESS-KEY": this.apiKey, 202 | "CB-ACCESS-SIGN": sign, 203 | "CB-ACCESS-TIMESTAMP": timestamp, 204 | "CB-ACCESS-PASSPHRASE": this.apiPassphrase 205 | }; 206 | 207 | const fullpath = this.apiURI + requestPath; 208 | 209 | //Call API: 210 | const result = await axios.post(fullpath, body, { headers }); 211 | 212 | return result.data; 213 | } catch (err) { 214 | if (attempts < numberOfAttempts - 1) { 215 | attempts++; 216 | } else { 217 | const message = "Error occurred in profileTransfer method. Number of attempts: " + numberOfAttempts; 218 | const errorMsg = new Error(err); 219 | logger.error({ message, errorMsg, err }); 220 | throw err; 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | module.exports = coinbaseProLib; -------------------------------------------------------------------------------- /buyAndSell.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This module contains methods to buy a position and sell a position. It uses a limit order then loops checking the order 3 | * status until the order either completes, OR after 1 minute it will cancel the order. 4 | */ 5 | const pino = require("pino"); 6 | const logger = pino({ level: process.env.LOG_LEVEL || "info" }); 7 | const fileSystem = require("fs"); 8 | 9 | /** 10 | * Halts the program from running temporarily to prevent it from hitting API call limits 11 | * 12 | * @param {number} ms -> the number of miliseconds to wait 13 | */ 14 | function sleep(ms) { 15 | return new Promise((resolve) => { 16 | setTimeout(resolve, ms); 17 | }); 18 | } 19 | 20 | /** 21 | * Places a sell limit order then loops to check the order status until the order is filled. Once filled, the method updates the positionInfo, does any depositing based on the 22 | * depositConfig, then ends. If the Order is done for a reason other than filled, or a profit was not made then the method throws an exception. If the order doesn't get filled 23 | * in the alloted time span (1 minute) then the method cancels the order and throws an exception. 24 | * 25 | * @param {Number} balance 26 | * @param {Object} accountIds 27 | * @param {Object} positionInfo 28 | * @param {Number} currentPrice 29 | * @param {Object} authedClient 30 | * @param {Object} coinbaseLibObject 31 | * @param {Object} productInfo 32 | * @param {Object} depositConfig 33 | * @param {Object} tradingConfig 34 | */ 35 | async function sellPosition(balance, accountIds, positionInfo, currentPrice, authedClient, coinbaseLibObject, productInfo, depositConfig, tradingConfig) { 36 | try { 37 | const priceToSell = (currentPrice - (currentPrice * tradingConfig.orderPriceDelta)).toFixed(productInfo.quoteIncrementRoundValue); 38 | 39 | let orderSize; 40 | if (productInfo.baseIncrementRoundValue === 0) { 41 | orderSize = Math.trunc(balance); 42 | } else { 43 | orderSize = (balance).toFixed(productInfo.baseIncrementRoundValue); 44 | } 45 | 46 | const orderParams = { 47 | side: "sell", 48 | price: priceToSell, 49 | size: orderSize, 50 | product_id: productInfo.productPair, 51 | time_in_force: "FOK" 52 | }; 53 | 54 | logger.info("Sell order params: " + JSON.stringify(orderParams)); 55 | 56 | //Place sell order 57 | const order = await authedClient.placeOrder(orderParams); 58 | logger.debug(order); 59 | const orderID = order.id; 60 | 61 | //Loop to wait for order to be filled: 62 | for (let i = 0; i < 100 && positionInfo.positionExists === true; ++i) { 63 | let orderDetails; 64 | logger.debug("Checking sell order result..."); 65 | await sleep(6000); //wait 6 seconds 66 | try { 67 | orderDetails = await authedClient.getOrder(orderID); //Get latest order details 68 | } catch (err) { 69 | const message = "Error occured when attempting to get the order."; 70 | const errorMsg = new Error(err); 71 | logger.error({ message, errorMsg, err }); 72 | continue; 73 | } 74 | logger.debug(orderDetails); 75 | 76 | if (orderDetails.status === "done") { 77 | if (orderDetails.done_reason !== "filled") { 78 | throw new Error("Sell order did not complete due to being filled? done_reason: " + orderDetails.done_reason); 79 | } else { 80 | positionInfo.positionExists = false; 81 | 82 | //Update positionData file: 83 | try { 84 | const writeData = JSON.stringify(positionInfo); 85 | fileSystem.writeFileSync("positionData.json", writeData); 86 | } catch (err) { 87 | const message = "Error, failed to write the positionInfo to the positionData file in sellPosition. Continuing as normal but but positionDataTracking might not work correctly."; 88 | const errorMsg = new Error(err); 89 | logger.error({ message, errorMsg, err }); 90 | } 91 | 92 | let profit = parseFloat(orderDetails.executed_value) - parseFloat(orderDetails.fill_fees) - positionInfo.positionAcquiredCost; 93 | logger.info("Profit: " + profit); 94 | 95 | if (profit > 0) { 96 | //Check deposit config: 97 | if (depositConfig.depositingEnabled) { 98 | const transferAmount = (profit * depositConfig.depositingAmount).toFixed(2); 99 | const currency = productInfo.quoteCurrency; 100 | 101 | //Transfer funds to depositProfileID 102 | const transferResult = await coinbaseLibObject.profileTransfer(accountIds.tradeProfileID, accountIds.depositProfileID, currency, transferAmount); 103 | 104 | logger.debug("transfer result: " + transferResult); 105 | } 106 | } else { 107 | throw new Error("Sell was not profitable, terminating program. profit: " + profit); 108 | } 109 | } 110 | } 111 | } 112 | 113 | //Check if order wasn't filled and needs cancelled: 114 | if (positionInfo.positionExists === true) { 115 | const cancelOrder = await authedClient.cancelOrder(orderID); 116 | if (cancelOrder !== orderID) { 117 | throw new Error("Attempted to cancel failed order but it did not work. cancelOrderReturn: " + cancelOrder + "orderID: " + orderID); 118 | } 119 | } 120 | 121 | } catch (err) { 122 | const message = "Error occurred in sellPosition method."; 123 | const errorMsg = new Error(err); 124 | logger.error({ message, errorMsg, err }); 125 | } 126 | } 127 | 128 | /** 129 | * This method places a buy limit order and loops waiting for it to be filled. Once filled it will update the positionInfo and end. If the 130 | * order ends for a reason other then filled it will throw an exception. If the order doesn't get filled after 1 minute it will cancel the 131 | * order and throw an exception. 132 | * 133 | * @param {Number} balance 134 | * @param {Object} positionInfo 135 | * @param {Number} currentPrice 136 | * @param {Object} authedClient 137 | * @param {Object} productInfo 138 | * @param {Object} tradingConfig 139 | */ 140 | async function buyPosition(balance, positionInfo, currentPrice, authedClient, productInfo, tradingConfig) { 141 | try { 142 | const amountToSpend = balance - (balance * tradingConfig.highestFee); 143 | const priceToBuy = (currentPrice + (currentPrice * tradingConfig.orderPriceDelta)).toFixed(productInfo.quoteIncrementRoundValue); 144 | let orderSize; 145 | 146 | if (productInfo.baseIncrementRoundValue === 0) { 147 | orderSize = Math.trunc(amountToSpend / priceToBuy); 148 | } else { 149 | orderSize = (amountToSpend / priceToBuy).toFixed(productInfo.baseIncrementRoundValue); 150 | } 151 | 152 | const orderParams = { 153 | side: "buy", 154 | price: priceToBuy, 155 | size: orderSize, 156 | product_id: productInfo.productPair, 157 | time_in_force: "FOK" 158 | }; 159 | 160 | logger.info("Buy order params: " + JSON.stringify(orderParams)); 161 | 162 | //Place buy order 163 | const order = await authedClient.placeOrder(orderParams); 164 | logger.debug(order); 165 | const orderID = order.id; 166 | 167 | //Loop to wait for order to be filled: 168 | for (let i = 0; i < 100 && positionInfo.positionExists === false; ++i) { 169 | let orderDetails; 170 | logger.debug("Checking buy order result..."); 171 | await sleep(6000); //wait 6 seconds 172 | try { 173 | orderDetails = await authedClient.getOrder(orderID); //Get latest order details 174 | } catch (err) { 175 | const message = "Error occured when attempting to get the order."; 176 | const errorMsg = new Error(err); 177 | logger.error({ message, errorMsg, err }); 178 | continue; 179 | } 180 | logger.debug(orderDetails); 181 | 182 | if (orderDetails.status === "done") { 183 | if (orderDetails.done_reason !== "filled") { 184 | throw new Error("Buy order did not complete due to being filled? done_reason: " + orderDetails.done_reason); 185 | } else { 186 | //Update position info 187 | positionInfo.positionExists = true; 188 | positionInfo.positionAcquiredPrice = parseFloat(orderDetails.executed_value) / parseFloat(orderDetails.filled_size); 189 | positionInfo.positionAcquiredCost = parseFloat(orderDetails.executed_value) + parseFloat(orderDetails.fill_fees); 190 | 191 | //Update positionData file: 192 | try { 193 | const writeData = JSON.stringify(positionInfo); 194 | fileSystem.writeFileSync("positionData.json", writeData); 195 | } catch (err) { 196 | const message = "Error, failed to write the positionInfo to the positionData file in buyPosition. Continuing as normal but but positionDataTracking might not work correctly."; 197 | const errorMsg = new Error(err); 198 | logger.error({ message, errorMsg, err }); 199 | } 200 | 201 | logger.info(positionInfo); 202 | } 203 | } 204 | } 205 | 206 | //Check if order wasn't filled and needs cancelled 207 | if (positionInfo.positionExists === false) { 208 | const cancelOrder = await authedClient.cancelOrder(orderID); 209 | if (cancelOrder !== orderID) { 210 | throw new Error("Attempted to cancel failed order but it did not work. cancelOrderReturn: " + cancelOrder + "orderID: " + orderID); 211 | } 212 | } 213 | 214 | } catch (err) { 215 | const message = "Error occurred in buyPosition method."; 216 | const errorMsg = new Error(err); 217 | logger.error({ message, errorMsg, err }); 218 | } 219 | } 220 | 221 | module.exports = { 222 | sellPosition, 223 | buyPosition, 224 | } -------------------------------------------------------------------------------- /strategies/momentumTrading/momentumTradingAnalyzer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Summary: The momentumTradingAnalyzer reads a CSV file for prices and runs the bot with a given configuration. 3 | * After processing all of the price history the analyzer gives report containing how much profit it made, how many trades 4 | * it made, etc. This can be used to test the bot against historical date and get an idea of how it performs with a specfic setup. 5 | * Consider creating a loop to test a range of values and let the analyzer figure out the most optimal trade configuration. 6 | * 7 | * For more information regarding the type of data and files that it's setup to use, see the readme. 8 | */ 9 | require('dotenv').config() 10 | const pino = require("pino"); 11 | const logger = pino({ level: process.env.LOG_LEVEL || "info" }); 12 | const fileSystem = require("fs").promises; 13 | const csvParser = require("csv-parse/lib/sync"); 14 | 15 | //***************Trade configuration***************** 16 | 17 | //The name of the file containing the data to be tested: 18 | const dataFileName = "xtzusd.csv"; 19 | 20 | //The bot trading config values (See momentumTrading.js for more information on these values): 21 | const tradingConfig = { 22 | startingBalance: 500, //Amount of cash the bot starts with 23 | sellPositionDelta: .0001, 24 | buyPositionDelta: .0001, 25 | orderPriceDelta: .001, 26 | highestFee: .005, 27 | depositingEnabled: false //Whether or not the profits are deposited or re-invested 28 | }; 29 | 30 | //*************************************************** 31 | 32 | /** 33 | * See losePosition in momentumTrading.js for more information, this is the same but for the analyzer 34 | * 35 | * @param {object} positionInfo 36 | * @param {object} tradingConfig 37 | * @param {object} priceInfo 38 | * @param {object} report 39 | */ 40 | async function losePosition(positionInfo, tradingConfig, priceInfo, report) { 41 | try { 42 | if (priceInfo.lastPeakPrice < priceInfo.currentPrice) { 43 | priceInfo.lastPeakPrice = priceInfo.currentPrice; 44 | priceInfo.lastValleyPrice = priceInfo.currentPrice; 45 | } else if (priceInfo.lastValleyPrice > priceInfo.currentPrice) { 46 | priceInfo.lastValleyPrice = priceInfo.currentPrice; 47 | 48 | const target = priceInfo.lastPeakPrice - (priceInfo.lastPeakPrice * tradingConfig.sellPositionDelta); 49 | const lowestSellPrice = priceInfo.lastValleyPrice - (priceInfo.lastValleyPrice * tradingConfig.orderPriceDelta); 50 | const receivedValue = (lowestSellPrice * positionInfo.assetAmount) - ((lowestSellPrice * positionInfo.assetAmount) * tradingConfig.highestFee); 51 | 52 | if ((priceInfo.lastValleyPrice <= target) && (receivedValue > positionInfo.positionAcquiredCost)) { 53 | //Sell position: 54 | logger.debug(`Sell position price: ${priceInfo.currentPrice}`); 55 | report.numberOfSells += 1; 56 | 57 | if (tradingConfig.depositingEnabled) { 58 | const profit = (positionInfo.assetAmount * priceInfo.currentPrice) - (tradingConfig.highestFee * (positionInfo.assetAmount * priceInfo.currentPrice)) - positionInfo.positionAcquiredCost; 59 | report.amountOfProfitGenerated += profit; 60 | logger.debug(`amount of profit: ${report.amountOfProfitGenerated}`); 61 | 62 | logger.debug(`profit: ${profit}`); 63 | 64 | positionInfo.fiatBalance = (positionInfo.assetAmount * priceInfo.currentPrice) - ((positionInfo.assetAmount * priceInfo.currentPrice) * tradingConfig.highestFee) - profit; 65 | } else { 66 | positionInfo.fiatBalance = (positionInfo.assetAmount * priceInfo.currentPrice) - ((positionInfo.assetAmount * priceInfo.currentPrice) * tradingConfig.highestFee); 67 | } 68 | 69 | positionInfo.assetAmount = 0; 70 | positionInfo.positionExists = false; 71 | positionInfo.positionAcquiredPrice = 0; 72 | positionInfo.positionAcquiredCost = 0; 73 | 74 | logger.debug(`Position info after sell: ${JSON.stringify(positionInfo)}`); 75 | } 76 | } 77 | } catch (err) { 78 | const message = "Error occured in losePosition method."; 79 | const errorMsg = new Error(err); 80 | logger.error({ message, errorMsg, err }); 81 | throw err; 82 | } 83 | } 84 | 85 | /** 86 | * See gainPosition in momentumTrading.js for more information, this is the same but for the analyzer 87 | * 88 | * @param {object} positionInfo 89 | * @param {object} tradingConfig 90 | * @param {object} priceInfo 91 | * @param {object} report 92 | */ 93 | async function gainPosition(positionInfo, tradingConfig, priceInfo, report) { 94 | try { 95 | if (priceInfo.lastPeakPrice < priceInfo.currentPrice) { 96 | priceInfo.lastPeakPrice = priceInfo.currentPrice; 97 | 98 | const target = priceInfo.lastValleyPrice + (priceInfo.lastValleyPrice * tradingConfig.buyPositionDelta); 99 | 100 | if (priceInfo.lastPeakPrice >= target) { 101 | //buy position: 102 | logger.debug(`Buy position price: ${priceInfo.currentPrice}`); 103 | report.numberOfBuys += 1; 104 | 105 | positionInfo.positionAcquiredCost = positionInfo.fiatBalance; 106 | positionInfo.assetAmount = (positionInfo.fiatBalance - (positionInfo.fiatBalance * tradingConfig.highestFee)) / priceInfo.currentPrice; 107 | positionInfo.positionAcquiredPrice = priceInfo.currentPrice; 108 | positionInfo.fiatBalance = 0; 109 | positionInfo.positionExists = true; 110 | 111 | logger.debug(`Position info after buy: ${JSON.stringify(positionInfo)}`); 112 | } 113 | } else if (priceInfo.lastValleyPrice > priceInfo.currentPrice) { 114 | priceInfo.lastPeakPrice = priceInfo.currentPrice; 115 | priceInfo.lastValleyPrice = priceInfo.currentPrice; 116 | } 117 | } catch (err) { 118 | const message = "Error occured in gainPosition method."; 119 | const errorMsg = new Error(err); 120 | logger.error({ message, errorMsg, err }); 121 | throw err; 122 | } 123 | } 124 | 125 | /** 126 | * Entry point, sets up and calls the analyze strategy method to begin. 127 | * This is the method someone could use to setup loops to test a range of trading config values to 128 | * find the optimal configuration for a given set of data. 129 | */ 130 | async function momentumStrategyAnalyzerStart() { 131 | try { 132 | //Run once: 133 | const report = await analyzeStrategy(tradingConfig, dataFileName); 134 | logger.info(report); 135 | 136 | //Instead of running it once someone could configure it to run loops for a given range of values to find the most optimal config 137 | //Just setup the tradingConfig to be your starting values then let the loops increment the values and run the report then compare for the most profitable 138 | //Example: 139 | 140 | // let highestProfit = {}; 141 | // let tradingConfigCopy = Object.assign({}, tradingConfig); 142 | 143 | // //baseline: 144 | // const report = await analyzeStrategy(tradingConfig, dataFileName); 145 | // highestProfit.report = report; 146 | // highestProfit.configuration = Object.assign({}, tradingConfig); 147 | 148 | // for (let i = 0; i < 50; i += 1) { 149 | // tradingConfigCopy.buyPositionDelta = tradingConfig.buyPositionDelta; 150 | 151 | // for (let j = 0; j < 50; j += 1) { 152 | // logger.debug(tradingConfig); 153 | 154 | // const report = await analyzeStrategy(tradingConfigCopy, dataFileName); 155 | 156 | // if (highestProfit.report.amountOfProfitGenerated < report.amountOfProfitGenerated) { 157 | // highestProfit.report = report; 158 | // highestProfit.configuration = Object.assign({}, tradingConfigCopy); 159 | 160 | // logger.info(highestProfit); 161 | // } 162 | 163 | // tradingConfigCopy.buyPositionDelta += .001; 164 | // } 165 | 166 | // tradingConfigCopy.sellPositionDelta += .001; 167 | // } 168 | 169 | // logger.info("Final Report:"); 170 | // logger.info(highestProfit); 171 | 172 | } catch (err) { 173 | const message = "Error occured in momentumStrategyAnalyzerStart method, shutting down. Check the logs for more information."; 174 | const errorMsg = new Error(err); 175 | logger.error({ message, errorMsg, err }); 176 | process.exit(1); 177 | } 178 | } 179 | 180 | /** 181 | * Tests the given tradingConfig against the data in the file to then returns a report on the results 182 | * 183 | * @param {object} tradingConfig 184 | * @param {string} dataFileName 185 | */ 186 | async function analyzeStrategy(tradingConfig, dataFileName) { 187 | try { 188 | let report = { 189 | numberOfBuys: 0, 190 | numberOfSells: 0, 191 | amountOfProfitGenerated: 0 192 | }; 193 | let positionInfo = { 194 | positionExists: false, 195 | fiatBalance: tradingConfig.startingBalance 196 | } 197 | 198 | const fileContent = await fileSystem.readFile(dataFileName); 199 | const records = csvParser(fileContent, { columns: true }); 200 | 201 | const priceInfo = { 202 | currentPrice: parseFloat(records[0].high), 203 | lastPeakPrice: parseFloat(records[0].high), 204 | lastValleyPrice: parseFloat(records[0].high) 205 | }; 206 | 207 | for (let i = 1; i < records.length; ++i) { 208 | priceInfo.currentPrice = parseFloat(records[i].high); 209 | 210 | if (positionInfo.positionExists) { 211 | await losePosition(positionInfo, tradingConfig, priceInfo, report); 212 | } else { 213 | await gainPosition(positionInfo, tradingConfig, priceInfo, report); 214 | } 215 | } 216 | 217 | if (positionInfo.positionExists) { 218 | report.amountOfProfitGenerated += ((positionInfo.assetAmount * priceInfo.currentPrice) - ((positionInfo.assetAmount * priceInfo.currentPrice) * tradingConfig.highestFee)) - tradingConfig.startingBalance; 219 | } else { 220 | if (!tradingConfig.depositingEnabled) { 221 | report.amountOfProfitGenerated = positionInfo.fiatBalance - tradingConfig.startingBalance; 222 | } 223 | } 224 | 225 | return report; 226 | 227 | } catch (err) { 228 | const message = "Error occured in analyzeStrategy method."; 229 | const errorMsg = new Error(err); 230 | logger.error({ message, errorMsg, err }); 231 | throw err; 232 | } 233 | } 234 | 235 | module.exports = momentumStrategyAnalyzerStart; -------------------------------------------------------------------------------- /strategies/reverseMomentumTrading/reverseMomentumTradingAnalyzer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Summary: The momentumTradingAnalyzer reads a CSV file for prices and runs the bot with a given configuration. 3 | * After processing all of the price history the analyzer gives report containing how much profit it made, how many trades 4 | * it made, etc. This can be used to test the bot against historical date and get an idea of how it performs with a specfic setup. 5 | * Consider creating a loop to test a range of values and let the analyzer figure out the most optimal trade configuration. 6 | * 7 | * For more information regarding the type of data and files that it's setup to use, see the readme. 8 | */ 9 | require('dotenv').config() 10 | const pino = require("pino"); 11 | const logger = pino({ level: process.env.LOG_LEVEL || "info" }); 12 | const fileSystem = require("fs").promises; 13 | const csvParser = require("csv-parse/lib/sync"); 14 | 15 | //***************Trade configuration***************** 16 | 17 | //The name of the file containing the data to be tested: 18 | const dataFileName = "btcusd.csv"; 19 | 20 | //The bot trading config values (See momentumTrading.js for more information on these values): 21 | const tradingConfig = { 22 | startingBalance: 500, //Amount of cash the bot starts with 23 | sellPositionDelta: .1, 24 | buyPositionDelta: .01, 25 | orderPriceDelta: .001, 26 | highestFee: .005, 27 | depositingEnabled: false //Whether or not the profits are deposited or re-invested 28 | }; 29 | 30 | //*************************************************** 31 | 32 | /** 33 | * See losePosition in momentumTrading.js for more information, this is the same but for the analyzer 34 | * 35 | * @param {object} positionInfo 36 | * @param {object} tradingConfig 37 | * @param {object} priceInfo 38 | * @param {object} report 39 | */ 40 | async function losePosition(positionInfo, tradingConfig, priceInfo, report) { 41 | try { 42 | if (priceInfo.lastPeakPrice < priceInfo.currentPrice) { 43 | priceInfo.lastPeakPrice = priceInfo.currentPrice; 44 | 45 | const target = priceInfo.lastValleyPrice + (priceInfo.lastValleyPrice * tradingConfig.sellPositionDelta); 46 | const lowestSellPrice = priceInfo.currentPrice - (priceInfo.currentPrice * tradingConfig.orderPriceDelta); 47 | const receivedValue = (lowestSellPrice * positionInfo.assetAmount) - ((lowestSellPrice * positionInfo.assetAmount) * tradingConfig.highestFee); 48 | 49 | if ((priceInfo.lastPeakPrice >= target) && (receivedValue > positionInfo.positionAcquiredCost)) { 50 | //Sell position: 51 | logger.debug(`Sell position price: ${priceInfo.currentPrice}`); 52 | report.numberOfSells += 1; 53 | 54 | if (tradingConfig.depositingEnabled) { 55 | const profit = (positionInfo.assetAmount * priceInfo.currentPrice) - (tradingConfig.highestFee * (positionInfo.assetAmount * priceInfo.currentPrice)) - positionInfo.positionAcquiredCost; 56 | report.amountOfProfitGenerated += profit; 57 | logger.debug(`amount of profit: ${report.amountOfProfitGenerated}`); 58 | 59 | logger.debug(`profit: ${profit}`); 60 | 61 | positionInfo.fiatBalance = (positionInfo.assetAmount * priceInfo.currentPrice) - ((positionInfo.assetAmount * priceInfo.currentPrice) * tradingConfig.highestFee) - profit; 62 | } else { 63 | positionInfo.fiatBalance = (positionInfo.assetAmount * priceInfo.currentPrice) - ((positionInfo.assetAmount * priceInfo.currentPrice) * tradingConfig.highestFee); 64 | } 65 | 66 | positionInfo.assetAmount = 0; 67 | positionInfo.positionExists = false; 68 | positionInfo.positionAcquiredPrice = 0; 69 | positionInfo.positionAcquiredCost = 0; 70 | 71 | logger.debug(`Position info after sell: ${JSON.stringify(positionInfo)}`); 72 | } 73 | } else if (priceInfo.lastValleyPrice > priceInfo.currentPrice) { 74 | priceInfo.lastPeakPrice = priceInfo.currentPrice; 75 | priceInfo.lastValleyPrice = priceInfo.currentPrice; 76 | } 77 | } catch (err) { 78 | const message = "Error occured in losePosition method."; 79 | const errorMsg = new Error(err); 80 | logger.error({ message, errorMsg, err }); 81 | throw err; 82 | } 83 | } 84 | 85 | /** 86 | * See gainPosition in momentumTrading.js for more information, this is the same but for the analyzer 87 | * 88 | * @param {object} positionInfo 89 | * @param {object} tradingConfig 90 | * @param {object} priceInfo 91 | * @param {object} report 92 | */ 93 | async function gainPosition(positionInfo, tradingConfig, priceInfo, report) { 94 | try { 95 | if (priceInfo.lastPeakPrice < priceInfo.currentPrice) { 96 | priceInfo.lastPeakPrice = priceInfo.currentPrice; 97 | priceInfo.lastValleyPrice = priceInfo.currentPrice; 98 | } else if (priceInfo.lastValleyPrice > priceInfo.currentPrice) { 99 | priceInfo.lastValleyPrice = priceInfo.currentPrice; 100 | 101 | const target = priceInfo.lastPeakPrice - (priceInfo.lastPeakPrice * tradingConfig.buyPositionDelta); 102 | 103 | if (priceInfo.lastValleyPrice <= target) { 104 | //buy position: 105 | logger.debug(`Buy position price: ${priceInfo.currentPrice}`); 106 | report.numberOfBuys += 1; 107 | 108 | positionInfo.positionAcquiredCost = positionInfo.fiatBalance; 109 | positionInfo.assetAmount = (positionInfo.fiatBalance - (positionInfo.fiatBalance * tradingConfig.highestFee)) / priceInfo.currentPrice; 110 | positionInfo.positionAcquiredPrice = priceInfo.currentPrice; 111 | positionInfo.fiatBalance = 0; 112 | positionInfo.positionExists = true; 113 | 114 | logger.debug(`Position info after buy: ${JSON.stringify(positionInfo)}`); 115 | } 116 | } 117 | } catch (err) { 118 | const message = "Error occured in gainPosition method."; 119 | const errorMsg = new Error(err); 120 | logger.error({ message, errorMsg, err }); 121 | throw err; 122 | } 123 | } 124 | 125 | /** 126 | * Entry point, sets up and calls the analyze strategy method to begin. 127 | * This is the method someone could use to setup loops to test a range of trading config values to 128 | * find the optimal configuration for a given set of data. 129 | */ 130 | async function reverseMomentumStrategyAnalyzerStart() { 131 | try { 132 | //Run once: 133 | const report = await analyzeStrategy(tradingConfig, dataFileName); 134 | logger.info(report); 135 | 136 | //Instead of running it once someone could configure it to run loops for a given range of values to find the most optimal config 137 | //Just setup the tradingConfig to be your starting values then let the loops increment the values and run the report then compare for the most profitable 138 | //Example: 139 | 140 | // let highestProfit = {}; 141 | // let tradingConfigCopy = Object.assign({}, tradingConfig); 142 | 143 | // //baseline: 144 | // const report = await analyzeStrategy(tradingConfig, dataFileName); 145 | // highestProfit.report = report; 146 | // highestProfit.configuration = Object.assign({}, tradingConfig); 147 | 148 | // for (let i = 0; i < 50; i += 1) { 149 | // tradingConfigCopy.buyPositionDelta = tradingConfig.buyPositionDelta; 150 | 151 | // for (let j = 0; j < 50; j += 1) { 152 | // logger.debug(tradingConfig); 153 | 154 | // const report = await analyzeStrategy(tradingConfigCopy, dataFileName); 155 | 156 | // if (highestProfit.report.amountOfProfitGenerated < report.amountOfProfitGenerated) { 157 | // highestProfit.report = report; 158 | // highestProfit.configuration = Object.assign({}, tradingConfigCopy); 159 | 160 | // logger.info(highestProfit); 161 | // } 162 | 163 | // tradingConfigCopy.buyPositionDelta += .001; 164 | // } 165 | 166 | // tradingConfigCopy.sellPositionDelta += .001; 167 | // } 168 | 169 | // logger.info("Final Report:"); 170 | // logger.info(highestProfit); 171 | 172 | } catch (err) { 173 | const message = "Error occured in momentumStrategyAnalyzerStart method, shutting down. Check the logs for more information."; 174 | const errorMsg = new Error(err); 175 | logger.error({ message, errorMsg, err }); 176 | process.exit(1); 177 | } 178 | } 179 | 180 | /** 181 | * Tests the given tradingConfig against the data in the file to then returns a report on the results 182 | * 183 | * @param {object} tradingConfig 184 | * @param {string} dataFileName 185 | */ 186 | async function analyzeStrategy(tradingConfig, dataFileName) { 187 | try { 188 | let report = { 189 | numberOfBuys: 0, 190 | numberOfSells: 0, 191 | amountOfProfitGenerated: 0 192 | }; 193 | let positionInfo = { 194 | positionExists: false, 195 | fiatBalance: tradingConfig.startingBalance 196 | } 197 | 198 | const fileContent = await fileSystem.readFile(dataFileName); 199 | const records = csvParser(fileContent, { columns: true }); 200 | 201 | const priceInfo = { 202 | currentPrice: parseFloat(records[0].high), 203 | lastPeakPrice: parseFloat(records[0].high), 204 | lastValleyPrice: parseFloat(records[0].high) 205 | }; 206 | 207 | for (let i = 1; i < records.length; ++i) { 208 | priceInfo.currentPrice = parseFloat(records[i].high); 209 | 210 | if (positionInfo.positionExists) { 211 | await losePosition(positionInfo, tradingConfig, priceInfo, report); 212 | } else { 213 | await gainPosition(positionInfo, tradingConfig, priceInfo, report); 214 | } 215 | } 216 | 217 | if (positionInfo.positionExists) { 218 | report.amountOfProfitGenerated += ((positionInfo.assetAmount * priceInfo.currentPrice) - ((positionInfo.assetAmount * priceInfo.currentPrice) * tradingConfig.highestFee)) - tradingConfig.startingBalance; 219 | } else { 220 | if (!tradingConfig.depositingEnabled) { 221 | report.amountOfProfitGenerated = positionInfo.fiatBalance - tradingConfig.startingBalance; 222 | } 223 | } 224 | 225 | return report; 226 | 227 | } catch (err) { 228 | const message = "Error occured in analyzeStrategy method."; 229 | const errorMsg = new Error(err); 230 | logger.error({ message, errorMsg, err }); 231 | throw err; 232 | } 233 | } 234 | 235 | module.exports = reverseMomentumStrategyAnalyzerStart; -------------------------------------------------------------------------------- /strategies/reverseMomentumTrading/reverseMomentumTrading.js: -------------------------------------------------------------------------------- 1 | const CoinbasePro = require("coinbase-pro"); 2 | require('dotenv').config() 3 | const { buyPosition, sellPosition } = require("../../buyAndSell"); 4 | const coinbaseProLib = require("../../coinbaseProLibrary"); 5 | const pino = require("pino"); 6 | const logger = pino({ level: process.env.LOG_LEVEL || "info" }); 7 | const fileSystem = require("fs"); 8 | 9 | const key = `${process.env.API_KEY}`; 10 | const secret = `${process.env.API_SECRET}`; 11 | const passphrase = `${process.env.API_PASSPHRASE}`; 12 | 13 | //******************** Setup these value configurations before running the program ****************************************** 14 | 15 | //Determines the enviornment, add TRADING_ENV=real to use the real enviornment otherwise defaults to the sandbox: 16 | const apiURI = process.env.TRADING_ENV === "real" ? "https://api.pro.coinbase.com" : "https://api-public.sandbox.pro.coinbase.com"; 17 | const websocketURI = process.env.TRADING_ENV === "real" ? "wss://ws-feed.pro.coinbase.com" : "wss://ws-feed-public.sandbox.pro.coinbase.com"; 18 | 19 | //Trading config: 20 | //Global constants, consider tuning these values to optimize the bot's trading: 21 | const sellPositionDelta = Number(process.env.SELL_POSITION_DELTA) || .02; //The amount of change between peak and valley to trigger a sell off 22 | const buyPositionDelta = Number(process.env.BUY_POSITION_DELTA) || .015; //The amount of change between the valley and peak price to trigger a buy in 23 | const orderPriceDelta = Number(process.env.ORDER_PRICE_DELTA) || .001; //The amount of extra room to give the sell/buy orders to go through 24 | 25 | //Currency config: 26 | //The pieces of the product pair, this is the two halves of coinbase product pair (examples of product pairs: BTC-USD, DASH-BTC, ETH-USDC). For BTC-USD the base currency is BTC and the quote currency is USD 27 | const baseCurrencyName = process.env.BASE_CURRENCY_NAME || "BTC"; 28 | const quoteCurrencyName = process.env.QUOTE_CURRENCY_NAME || "USD"; 29 | 30 | //Profile config: 31 | //Coinbase portfolios (profiles): 32 | const tradingProfileName = process.env.TRADING_PROFILE_NAME || "BTC trader"; //This is the name of the profile you want the bot to trade in 33 | const depositProfileName = process.env.DEPOSIT_PROFILE_NAME || "default"; //This is the name of the profile you want to deposit some profits to 34 | 35 | //Deposit config: 36 | const depositingEnabled = process.env.DEPOSITING_ENABLED !== "false"; //Choose whether or not you want you want to deposit a cut of the profits (Options: true/false) 37 | const depositingAmount = Number(process.env.DEPOSITING_AMOUNT) || 0.5; //Enter the amount of profit you want deposited (Options: choose a percent between 1 and 100 in decimal form I.E. .5 = 50%) 38 | 39 | // Due to rounding errors the buy order may not have enough funds to execute the order. This is the minimum funds amount in dollars that 40 | // will be left in usd account to avoid this error. Default = 6 cents (.06). 41 | const balanceMinimum = Number(process.env.BALANCE_MINIMUM) || .06; 42 | 43 | //*************************************************************************************************************************** 44 | 45 | //authedClient used to the API calls supported by the coinbase pro api node library 46 | let authedClient = new CoinbasePro.AuthenticatedClient( 47 | key, 48 | secret, 49 | passphrase, 50 | apiURI 51 | ); 52 | 53 | //Custom coinbase library used for making the calls not supported by the coinbase pro api node library 54 | const coinbaseLibObject = new coinbaseProLib(key, secret, passphrase, apiURI); 55 | 56 | //Global variable tracks the currentPrice. Updated by the websocket 57 | let currentPrice; 58 | 59 | /** 60 | * Makes the program sleep to avoid hitting API limits and let the websocket update 61 | * 62 | * @param {number} ms -> the number of miliseconds to wait 63 | */ 64 | function sleep(ms) { 65 | return new Promise((resolve) => { 66 | setTimeout(resolve, ms); 67 | }); 68 | } 69 | 70 | /** 71 | * Creates the websocket object and turns it on to update the currentPrice 72 | * 73 | * @param {string} productPair 74 | */ 75 | function listenForPriceUpdates(productPair) { 76 | if (productPair == null) { 77 | throw new Error("Error in listenForPriceUpdates method. ProductPair is null!"); 78 | } 79 | 80 | // The websocket client provides price updates on the product, refer to the docs for more information 81 | const websocket = new CoinbasePro.WebsocketClient( 82 | [productPair], 83 | websocketURI, 84 | { 85 | key, 86 | secret, 87 | passphrase, 88 | }, 89 | { channels: ["ticker"] } 90 | ); 91 | 92 | //turn on the websocket for errors 93 | websocket.on("error", function (err) { 94 | const message = "Error occured in the websocket."; 95 | const errorMsg = new Error(err); 96 | logger.error({ message, errorMsg, err }); 97 | listenForPriceUpdates(productPair); 98 | }); 99 | 100 | //Turn on the websocket for closes to restart it 101 | websocket.on("close", function () { 102 | logger.debug("WebSocket closed, restarting..."); 103 | listenForPriceUpdates(productPair); 104 | }); 105 | 106 | //Turn on the websocket for messages 107 | websocket.on("message", function (data) { 108 | if (data.type === "ticker") { 109 | if (currentPrice !== data.price) { 110 | currentPrice = parseFloat(data.price); 111 | logger.debug("Ticker price: " + currentPrice); 112 | } 113 | } 114 | }); 115 | } 116 | 117 | /** 118 | * Loops forever until the conditions are right to attempt to sell the position. Every loop sleeps to let the currentPrice update 119 | * then updates the lastPeak/lastValley price as appropiate, if the price hits a new valley price it will check if the conditions are 120 | * met to sell the position and call the method if appropiate. 121 | * 122 | * @param {number} balance The amount of currency being traded with 123 | * @param {number} lastPeakPrice Tracks the price highs 124 | * @param {number} lastValleyPrice Tracks the price lows 125 | * @param {Object} accountIds The coinbase account ID associated with the API key used for storing a chunk of the profits in coinbase 126 | * @param {Object} positionInfo Contains 3 fields, positionExists (bool), positionAcquiredPrice (number), and positionAcquiredCost(number) 127 | * @param {Object} productInfo Contains information about the quote/base increment for the product pair 128 | * @param {Object} depositConfig Conatins information about whether to do a deposit and for how much after a sell 129 | * @param {Object} tradingConfig Contains information about the fees and deltas 130 | */ 131 | async function losePosition(balance, lastPeakPrice, lastValleyPrice, accountIds, positionInfo, productInfo, depositConfig, tradingConfig) { 132 | try { 133 | while (positionInfo.positionExists === true) { 134 | await sleep(250); //Let price update 135 | 136 | if (lastPeakPrice < currentPrice) { 137 | //New peak hit, track peak price and check buy conditions 138 | lastPeakPrice = currentPrice; 139 | 140 | const target = lastValleyPrice + (lastValleyPrice * sellPositionDelta); 141 | const lowestSellPrice = lastPeakPrice - (lastPeakPrice * orderPriceDelta); 142 | const receivedValue = (lowestSellPrice * balance) - ((lowestSellPrice * balance) * tradingConfig.highestFee); 143 | 144 | logger.debug(`Sell Position, current price: ${lastPeakPrice} needs to be greater than or equal to ${target} to sell and the receivedValue: ${receivedValue} needs to be greater than the positionAcquiredCost: ${positionInfo.positionAcquiredCost}`); 145 | 146 | if ((lastPeakPrice >= target) && (receivedValue > positionInfo.positionAcquiredCost)) { 147 | logger.info("Attempting to sell position..."); 148 | 149 | //Create a new authenticated client to prevent it from expiring or hitting API limits 150 | authedClient = new CoinbasePro.AuthenticatedClient( 151 | key, 152 | secret, 153 | passphrase, 154 | apiURI 155 | ); 156 | 157 | await sellPosition(balance, accountIds, positionInfo, lastValleyPrice, authedClient, coinbaseLibObject, productInfo, depositConfig, tradingConfig); 158 | } 159 | } else if (lastValleyPrice > currentPrice) { 160 | //New valley hit, reset values 161 | lastPeakPrice = currentPrice; 162 | lastValleyPrice = currentPrice; 163 | } 164 | } 165 | } catch (err) { 166 | const message = "Error occured in losePosition method."; 167 | const errorMsg = new Error(err); 168 | logger.error({ message, errorMsg, err }); 169 | throw err; 170 | } 171 | } 172 | 173 | /** 174 | * Loops forever until the conditions are right to attempt to buy a position. Every loop sleeps to let the currentPrice update 175 | * then updates the lastPeak/lastValley price as appropiate, if the price hits a new peak price it will check if the conditions are 176 | * met to buy the position and call the method if appropiate. 177 | * 178 | * @param {number} balance The amount of currency being traded with 179 | * @param {number} lastPeakPrice Tracks the price highs 180 | * @param {number} lastValleyPrice Tracks the price lows 181 | * @param {Object} positionInfo Contains 3 fields, positionExists (bool), positionAcquiredPrice (number), and positionAcquiredCost(number) 182 | * @param {Object} productInfo Contains information about the quote/base increment for the product pair 183 | * @param {Object} tradingConfig Contains information about the fees and deltas 184 | */ 185 | async function gainPosition(balance, lastPeakPrice, lastValleyPrice, positionInfo, productInfo, tradingConfig) { 186 | try { 187 | while (positionInfo.positionExists === false) { 188 | await sleep(250); //Let price update 189 | 190 | if (lastPeakPrice < currentPrice) { 191 | //New peak hit, reset values 192 | lastPeakPrice = currentPrice; 193 | lastValleyPrice = currentPrice; 194 | } else if (lastValleyPrice > currentPrice) { 195 | //New valley hit, track valley and check buy conditions 196 | lastValleyPrice = currentPrice; 197 | 198 | const target = lastPeakPrice - (lastPeakPrice * buyPositionDelta); 199 | 200 | logger.debug(`Buy Position, current price: ${lastValleyPrice} needs to be less than or equal to ${target} to buy`); 201 | 202 | if (lastValleyPrice <= target) { 203 | logger.info("Attempting to buy position..."); 204 | 205 | //Create a new authenticated client to prevent it from expiring or hitting API limits 206 | authedClient = new CoinbasePro.AuthenticatedClient( 207 | key, 208 | secret, 209 | passphrase, 210 | apiURI 211 | ); 212 | 213 | await buyPosition(balance, positionInfo, lastPeakPrice, authedClient, productInfo, tradingConfig); 214 | } 215 | } 216 | } 217 | } catch (err) { 218 | const message = "Error occured in gainPosition method."; 219 | const errorMsg = new Error(err); 220 | logger.error({ message, errorMsg, err }); 221 | throw err; 222 | } 223 | } 224 | 225 | /** 226 | * Acquires some account ID information to be used for storing and retrieving information and depositing funds after a sell. 227 | * 228 | * @param {Object} productInfo productInfo contains the base and quote currencies being traded with needed to grab the correct account IDs 229 | * @return {Object} accountObject contains the needed account IDs and profile IDs needed for checking balances and making transfers 230 | */ 231 | async function getAccountIDs(productInfo) { 232 | try { 233 | let accountObject = {}; 234 | 235 | //Gets the account IDs for the product pairs in the portfolio 236 | const accounts = await authedClient.getAccounts(); 237 | 238 | for (let i = 0; i < accounts.length; ++i) { 239 | if (accounts[i].currency === productInfo.baseCurrency) { 240 | accountObject.baseCurrencyAccountID = accounts[i].id; 241 | } else if (accounts[i].currency === productInfo.quoteCurrency) { 242 | accountObject.quoteCurrencyAccountID = accounts[i].id; 243 | } 244 | } 245 | 246 | //Gets all the profiles belonging to the user and matches the deposit and trading profile IDs 247 | const profiles = await coinbaseLibObject.getProfiles(); 248 | 249 | for (let i = 0; i < profiles.length; ++i) { 250 | if (profiles[i].name === depositProfileName) { 251 | accountObject.depositProfileID = profiles[i].id; 252 | } else if (profiles[i].name === tradingProfileName) { 253 | accountObject.tradeProfileID = profiles[i].id; 254 | } 255 | } 256 | 257 | if (!accountObject.depositProfileID) { 258 | throw new Error(`Could not find the deposit profile ID. Ensure that the depositProfileName: "${depositProfileName}" is spelt correctly.`) 259 | } 260 | if (!accountObject.tradeProfileID) { 261 | throw new Error(`Could not find the trade profile ID. Ensure that the tradingProfileName: "${tradingProfileName}" is spelt correctly.`) 262 | } 263 | 264 | return accountObject; 265 | } catch (err) { 266 | const message = "Error occured in getAccountIDs method."; 267 | const errorMsg = new Error(err); 268 | logger.error({ message, errorMsg, err }); 269 | throw err; 270 | } 271 | } 272 | 273 | /** 274 | * Gets information about the product being traded that the bot can use to determine how 275 | * accurate the size and quote values for the order needs to be. This method parses the base and quote increment 276 | * strings in order to determine to what precision the size and price parameters need to be when placing an order. 277 | * 278 | * @param {object} productInfo This object gets updated directly 279 | */ 280 | async function getProductInfo(productInfo) { 281 | try { 282 | let quoteIncrementRoundValue = 0; 283 | let baseIncrementRoundValue = 0; 284 | let productPairData; 285 | 286 | const products = await authedClient.getProducts(); 287 | 288 | for (let i = 0; i < products.length; ++i) { 289 | if (products[i].id === productInfo.productPair) { 290 | productPairData = products[i]; 291 | } 292 | } 293 | 294 | if (productPairData === undefined) { 295 | throw new Error(`Error, could not find a valid matching product pair for "${productInfo.productPair}". Verify the product names is correct/exists.`); 296 | } 297 | 298 | for (let i = 2; i < productPairData.quote_increment.length; ++i) { 299 | if (productPairData.quote_increment[i] === "1") { 300 | quoteIncrementRoundValue++; 301 | break; 302 | } else { 303 | quoteIncrementRoundValue++; 304 | } 305 | } 306 | 307 | if (productPairData.base_increment[0] !== "1") { 308 | for (let i = 2; i < productPairData.base_increment.length; ++i) { 309 | if (productPairData.base_increment[i] === "1") { 310 | baseIncrementRoundValue++; 311 | break; 312 | } else { 313 | baseIncrementRoundValue++; 314 | } 315 | } 316 | } 317 | 318 | productInfo.quoteIncrementRoundValue = Number(quoteIncrementRoundValue); 319 | productInfo.baseIncrementRoundValue = Number(baseIncrementRoundValue); 320 | } catch (err) { 321 | const message = "Error occured in getProfuctInfo method."; 322 | const errorMsg = new Error(err); 323 | logger.error({ message, errorMsg, err }); 324 | throw err; 325 | } 326 | } 327 | 328 | /** 329 | * Retrieves the current maker and taker fees and returns the highest one as a number 330 | * 331 | * @param {number} highestFee The highest fee between the taker and maker fee 332 | */ 333 | async function returnHighestFee() { 334 | try { 335 | const feeResult = await coinbaseLibObject.getFees(); 336 | 337 | let makerFee = parseFloat(feeResult.maker_fee_rate); 338 | let takerFee = parseFloat(feeResult.taker_fee_rate); 339 | 340 | if (makerFee > takerFee) { 341 | return makerFee; 342 | } else { 343 | return takerFee; 344 | } 345 | } 346 | catch (err) { 347 | const message = "Error occured in getFees method."; 348 | const errorMsg = new Error(err); 349 | logger.error({ message, errorMsg, err }); 350 | throw err; 351 | } 352 | } 353 | 354 | /** 355 | * This method is the entry point of the momentum strategy. It does some first time initialization then begins an infinite loop. 356 | * The loop checks the position info to decide if the bot needs to try and buy or sell, it also checks if there's an available 357 | * balance to be traded with. Then it calls gainPosition or losePosition appropiately and waits for them to finish and repeats. 358 | */ 359 | async function reverseMomentumStrategyStart() { 360 | try { 361 | logger.info(`Configuration:\napiURI: ${apiURI}\nwebsocketURI: ${websocketURI}\nsellPositionDelta: ${sellPositionDelta}\nbuyPositionDelta: ${buyPositionDelta}\norderPriceDelta: ${orderPriceDelta}\nbaseCurrencyName: ${baseCurrencyName}\nquoteCurrencyName: ${quoteCurrencyName}\ntradingProfileName: ${tradingProfileName}\ndepositProfileName: ${depositProfileName}\ndepositingEnabled: ${depositingEnabled}\ndepositingAmount: ${depositingAmount}\nbalanceMinimum: ${balanceMinimum}`); 362 | 363 | let accountIDs = {}; 364 | let lastPeakPrice; 365 | let lastValleyPrice; 366 | let highestFee = await returnHighestFee(); 367 | 368 | const tradingConfig = { 369 | sellPositionDelta, 370 | buyPositionDelta, 371 | orderPriceDelta, 372 | highestFee 373 | }; 374 | 375 | const depositConfig = { 376 | depositingEnabled, 377 | depositingAmount 378 | }; 379 | 380 | const productInfo = { 381 | baseCurrency: baseCurrencyName, 382 | quoteCurrency: quoteCurrencyName, 383 | productPair: baseCurrencyName + "-" + quoteCurrencyName 384 | }; 385 | 386 | let positionInfo; 387 | 388 | //Check for an existing positionData file to start the bot with: 389 | try { 390 | //read positionData file: 391 | let rawFileData = fileSystem.readFileSync("positionData.json"); 392 | positionInfo = JSON.parse(rawFileData); 393 | logger.info("Found positionData.json file, starting with position data. Position data: " + JSON.stringify(positionInfo)); 394 | } catch (err) { 395 | if (err.code === "ENOENT") { 396 | logger.info("No positionData file found, starting with no existing position."); 397 | } else { 398 | const message = "Error, failed to read file for a reason other than it doesn't exist. Continuing as normal but positionDataTracking might not work correctly."; 399 | const errorMsg = new Error(err); 400 | logger.error({ message, errorMsg, err }); 401 | } 402 | 403 | positionInfo = { 404 | positionExists: false, 405 | }; 406 | } 407 | 408 | //Retrieve product information: 409 | await getProductInfo(productInfo); 410 | logger.info(productInfo); 411 | 412 | //Retrieve account IDs: 413 | accountIDs = await getAccountIDs(productInfo); 414 | logger.info(accountIDs) 415 | 416 | //activate websocket for price data: 417 | listenForPriceUpdates(productInfo.productPair); 418 | 419 | while (currentPrice == null) { 420 | await sleep(1000); //Get a price before starting 421 | } 422 | 423 | logger.info(`Starting price of ${productInfo.baseCurrency} in ${productInfo.quoteCurrency} is: ${currentPrice}`); 424 | 425 | // eslint-disable-next-line no-constant-condition 426 | while (true) { 427 | if (positionInfo.positionExists) { 428 | tradingConfig.highestFee = await returnHighestFee(); 429 | await sleep(1000); 430 | const baseCurrencyAccount = await authedClient.getAccount(accountIDs.baseCurrencyAccountID); //Grab account information to view balance 431 | 432 | if (baseCurrencyAccount.available > 0) { 433 | logger.info("Entering lose position with: " + baseCurrencyAccount.available + " " + productInfo.baseCurrency); 434 | 435 | lastPeakPrice = currentPrice; 436 | lastValleyPrice = currentPrice; 437 | 438 | //Begin trying to sell position: 439 | await losePosition(parseFloat(baseCurrencyAccount.available), lastPeakPrice, lastValleyPrice, accountIDs, positionInfo, productInfo, depositConfig, tradingConfig); 440 | } else { 441 | throw new Error(`Error, there is no ${productInfo.baseCurrency} balance available for use. Terminating program.`); 442 | } 443 | } else { 444 | tradingConfig.highestFee = await returnHighestFee(); 445 | await sleep(1000); 446 | const quoteCurrencyAccount = await authedClient.getAccount(accountIDs.quoteCurrencyAccountID); //Grab account information to view balance 447 | const availableBalance = parseFloat(quoteCurrencyAccount.available); 448 | 449 | if (availableBalance > 0) { 450 | const tradeBalance = availableBalance - balanceMinimum; //Subtract this dollar amount so that there is room for rounding errors 451 | 452 | logger.info("Entering gain position with: " + tradeBalance + " " + productInfo.quoteCurrency); 453 | 454 | lastPeakPrice = currentPrice; 455 | lastValleyPrice = currentPrice; 456 | 457 | //Begin trying to buy a position: 458 | await gainPosition(tradeBalance, lastPeakPrice, lastValleyPrice, positionInfo, productInfo, tradingConfig); 459 | } else { 460 | throw new Error(`Error, there is no ${productInfo.quoteCurrency} balance available for use. Terminating program.`); 461 | } 462 | } 463 | } 464 | } catch (err) { 465 | const message = "Error occured in bot, shutting down. Check the logs for more information."; 466 | const errorMsg = new Error(err); 467 | logger.error({ message, errorMsg, err }); 468 | process.exit(1); 469 | } 470 | } 471 | 472 | module.exports = reverseMomentumStrategyStart; -------------------------------------------------------------------------------- /strategies/momentumTrading/momentumTrading.js: -------------------------------------------------------------------------------- 1 | const CoinbasePro = require("coinbase-pro"); 2 | require('dotenv').config() 3 | const { buyPosition, sellPosition } = require("../../buyAndSell"); 4 | const coinbaseProLib = require("../../coinbaseProLibrary"); 5 | const pino = require("pino"); 6 | const logger = pino({ level: process.env.LOG_LEVEL || "info" }); 7 | const fileSystem = require("fs"); 8 | 9 | const key = `${process.env.API_KEY}`; 10 | const secret = `${process.env.API_SECRET}`; 11 | const passphrase = `${process.env.API_PASSPHRASE}`; 12 | 13 | //******************** Setup these value configurations before running the program ****************************************** 14 | 15 | //Determines the enviornment, add TRADING_ENV=real to use the real enviornment otherwise defaults to the sandbox: 16 | const apiURI = process.env.TRADING_ENV === "real" ? "https://api.pro.coinbase.com" : "https://api-public.sandbox.pro.coinbase.com"; 17 | const websocketURI = process.env.TRADING_ENV === "real" ? "wss://ws-feed.pro.coinbase.com" : "wss://ws-feed-public.sandbox.pro.coinbase.com"; 18 | 19 | //Trading config: 20 | //Global constants, consider tuning these values to optimize the bot's trading: 21 | const sellPositionDelta = Number(process.env.SELL_POSITION_DELTA) || .02; //The amount of change between peak and valley to trigger a sell off 22 | const buyPositionDelta = Number(process.env.BUY_POSITION_DELTA) || .015; //The amount of change between the valley and peak price to trigger a buy in 23 | const orderPriceDelta = Number(process.env.ORDER_PRICE_DELTA) || .001; //The amount of extra room to give the sell/buy orders to go through 24 | 25 | //Currency config: 26 | //The pieces of the product pair, this is the two halves of coinbase product pair (examples of product pairs: BTC-USD, DASH-BTC, ETH-USDC). For BTC-USD the base currency is BTC and the quote currency is USD 27 | const baseCurrencyName = process.env.BASE_CURRENCY_NAME || "BTC"; 28 | const quoteCurrencyName = process.env.QUOTE_CURRENCY_NAME || "USD"; 29 | 30 | //Profile config: 31 | //Coinbase portfolios (profiles): 32 | const tradingProfileName = process.env.TRADING_PROFILE_NAME || "BTC trader"; //This is the name of the profile you want the bot to trade in 33 | const depositProfileName = process.env.DEPOSIT_PROFILE_NAME || "default"; //This is the name of the profile you want to deposit some profits to 34 | 35 | //Deposit config: 36 | const depositingEnabled = process.env.DEPOSITING_ENABLED !== "false"; //Choose whether or not you want you want to deposit a cut of the profits (Options: true/false) 37 | const depositingAmount = Number(process.env.DEPOSITING_AMOUNT) || 0.5; //Enter the amount of profit you want deposited (Options: choose a percent between 1 and 100 in decimal form I.E. .5 = 50%) 38 | 39 | // Due to rounding errors the buy order may not have enough funds to execute the order. This is the minimum funds amount in dollars that 40 | // will be left in usd account to avoid this error. Default = 6 cents (.06). 41 | const balanceMinimum = Number(process.env.BALANCE_MINIMUM) || .06; 42 | 43 | //*************************************************************************************************************************** 44 | 45 | //authedClient used to the API calls supported by the coinbase pro api node library 46 | let authedClient = new CoinbasePro.AuthenticatedClient( 47 | key, 48 | secret, 49 | passphrase, 50 | apiURI 51 | ); 52 | 53 | //Custom coinbase library used for making the calls not supported by the coinbase pro api node library 54 | const coinbaseLibObject = new coinbaseProLib(key, secret, passphrase, apiURI); 55 | 56 | //Global variable tracks the currentPrice. Updated by the websocket 57 | let currentPrice; 58 | 59 | /** 60 | * Makes the program sleep to avoid hitting API limits and let the websocket update 61 | * 62 | * @param {number} ms -> the number of milliseconds to wait 63 | */ 64 | function sleep(ms) { 65 | return new Promise((resolve) => { 66 | setTimeout(resolve, ms); 67 | }); 68 | } 69 | 70 | /** 71 | * Creates the websocket object and turns it on to update the currentPrice 72 | * 73 | * @param {string} productPair 74 | */ 75 | function listenForPriceUpdates(productPair) { 76 | if (productPair == null) { 77 | throw new Error("Error in listenForPriceUpdates method. ProductPair is null!"); 78 | } 79 | 80 | // The websocket client provides price updates on the product, refer to the docs for more information 81 | const websocket = new CoinbasePro.WebsocketClient( 82 | [productPair], 83 | websocketURI, 84 | { 85 | key, 86 | secret, 87 | passphrase, 88 | }, 89 | { channels: ["ticker"] } 90 | ); 91 | 92 | //turn on the websocket for errors 93 | websocket.on("error", function (err) { 94 | const message = "Error occurred in the websocket."; 95 | const errorMsg = new Error(err); 96 | logger.error({ message, errorMsg, err }); 97 | listenForPriceUpdates(productPair); 98 | }); 99 | 100 | //Turn on the websocket for closes to restart it 101 | websocket.on("close", function () { 102 | logger.debug("WebSocket closed, restarting..."); 103 | listenForPriceUpdates(productPair); 104 | }); 105 | 106 | //Turn on the websocket for messages 107 | websocket.on("message", function (data) { 108 | if (data.type === "ticker") { 109 | if (currentPrice !== data.price) { 110 | currentPrice = parseFloat(data.price); 111 | logger.debug("Ticker price: " + currentPrice); 112 | } 113 | } 114 | }); 115 | } 116 | 117 | /** 118 | * Loops forever until the conditions are right to attempt to sell the position. Every loop sleeps to let the currentPrice update 119 | * then updates the lastPeak/lastValley price as appropriate, if the price hits a new valley price it will check if the conditions are 120 | * met to sell the position and call the method if appropriate. 121 | * 122 | * @param {number} balance The amount of currency being traded with 123 | * @param {number} lastPeakPrice Tracks the price highs 124 | * @param {number} lastValleyPrice Tracks the price lows 125 | * @param {Object} accountIds The coinbase account ID associated with the API key used for storing a chunk of the profits in coinbase 126 | * @param {Object} positionInfo Contains 3 fields, positionExists (bool), positionAcquiredPrice (number), and positionAcquiredCost(number) 127 | * @param {Object} productInfo Contains information about the quote/base increment for the product pair 128 | * @param {Object} depositConfig Contains information about whether to do a deposit and for how much after a sell 129 | * @param {Object} tradingConfig Contains information about the fees and deltas 130 | */ 131 | async function losePosition(balance, lastPeakPrice, lastValleyPrice, accountIds, positionInfo, productInfo, depositConfig, tradingConfig) { 132 | try { 133 | while (positionInfo.positionExists === true) { 134 | await sleep(250); //Let price update 135 | 136 | if (lastPeakPrice < currentPrice) { 137 | //New peak hit, reset values 138 | lastPeakPrice = currentPrice; 139 | lastValleyPrice = currentPrice; 140 | 141 | logger.debug(`Sell Position, LPP: ${lastPeakPrice}`); 142 | } else if (lastValleyPrice > currentPrice) { 143 | //New valley hit, track valley and check sell conditions 144 | lastValleyPrice = currentPrice; 145 | 146 | const target = lastPeakPrice - (lastPeakPrice * sellPositionDelta); 147 | const lowestSellPrice = lastValleyPrice - (lastValleyPrice * orderPriceDelta); 148 | const receivedValue = (lowestSellPrice * balance) - ((lowestSellPrice * balance) * tradingConfig.highestFee); 149 | 150 | logger.debug(`Sell Position, LVP: ${lastValleyPrice} needs to be less than or equal to ${target} to sell and the receivedValue: ${receivedValue} needs to be greater than the positionAcquiredCost: ${positionInfo.positionAcquiredCost}`); 151 | 152 | if ((lastValleyPrice <= target) && (receivedValue > positionInfo.positionAcquiredCost)) { 153 | logger.info("Attempting to sell position..."); 154 | 155 | //Create a new authenticated client to prevent it from expiring or hitting API limits 156 | authedClient = new CoinbasePro.AuthenticatedClient( 157 | key, 158 | secret, 159 | passphrase, 160 | apiURI 161 | ); 162 | 163 | await sellPosition(balance, accountIds, positionInfo, lastValleyPrice, authedClient, coinbaseLibObject, productInfo, depositConfig, tradingConfig); 164 | } 165 | } 166 | } 167 | } catch (err) { 168 | const message = "Error occurred in losePosition method."; 169 | const errorMsg = new Error(err); 170 | logger.error({ message, errorMsg, err }); 171 | throw err; 172 | } 173 | } 174 | 175 | /** 176 | * Loops forever until the conditions are right to attempt to buy a position. Every loop sleeps to let the currentPrice update 177 | * then updates the lastPeak/lastValley price as appropriate, if the price hits a new peak price it will check if the conditions are 178 | * met to buy the position and call the method if appropriate. 179 | * 180 | * @param {number} balance The amount of currency being traded with 181 | * @param {number} lastPeakPrice Tracks the price highs 182 | * @param {number} lastValleyPrice Tracks the price lows 183 | * @param {Object} positionInfo Contains 3 fields, positionExists (bool), positionAcquiredPrice (number), and positionAcquiredCost(number) 184 | * @param {Object} productInfo Contains information about the quote/base increment for the product pair 185 | * @param {Object} tradingConfig Contains information about the fees and deltas 186 | */ 187 | async function gainPosition(balance, lastPeakPrice, lastValleyPrice, positionInfo, productInfo, tradingConfig) { 188 | try { 189 | while (positionInfo.positionExists === false) { 190 | await sleep(250); //Let price update 191 | 192 | if (lastPeakPrice < currentPrice) { 193 | //New peak hit, track peak price and check buy conditions 194 | lastPeakPrice = currentPrice; 195 | 196 | const target = lastValleyPrice + (lastValleyPrice * buyPositionDelta); 197 | 198 | logger.debug(`Buy Position, LPP: ${lastPeakPrice} needs to be greater than or equal to ${target} to buy`); 199 | 200 | if (lastPeakPrice >= target) { 201 | logger.info("Attempting to buy position..."); 202 | 203 | //Create a new authenticated client to prevent it from expiring or hitting API limits 204 | authedClient = new CoinbasePro.AuthenticatedClient( 205 | key, 206 | secret, 207 | passphrase, 208 | apiURI 209 | ); 210 | 211 | await buyPosition(balance, positionInfo, lastPeakPrice, authedClient, productInfo, tradingConfig); 212 | } 213 | } else if (lastValleyPrice > currentPrice) { 214 | //New valley hit, reset values 215 | 216 | lastPeakPrice = currentPrice; 217 | lastValleyPrice = currentPrice; 218 | 219 | logger.debug(`Buy Position, LVP: ${lastValleyPrice}`); 220 | } 221 | } 222 | } catch (err) { 223 | const message = "Error occurred in gainPosition method."; 224 | const errorMsg = new Error(err); 225 | logger.error({ message, errorMsg, err }); 226 | throw err; 227 | } 228 | } 229 | 230 | /** 231 | * Acquires some account ID information to be used for storing and retrieving information and depositing funds after a sell. 232 | * 233 | * @param {Object} productInfo productInfo contains the base and quote currencies being traded with needed to grab the correct account IDs 234 | * @return {Object} accountObject contains the needed account IDs and profile IDs needed for checking balances and making transfers 235 | */ 236 | async function getAccountIDs(productInfo) { 237 | try { 238 | let accountObject = {}; 239 | 240 | //Gets the account IDs for the product pairs in the portfolio 241 | const accounts = await authedClient.getAccounts(); 242 | 243 | for (let i = 0; i < accounts.length; ++i) { 244 | if (accounts[i].currency === productInfo.baseCurrency) { 245 | accountObject.baseCurrencyAccountID = accounts[i].id; 246 | } else if (accounts[i].currency === productInfo.quoteCurrency) { 247 | accountObject.quoteCurrencyAccountID = accounts[i].id; 248 | } 249 | } 250 | 251 | //Gets all the profiles belonging to the user and matches the deposit and trading profile IDs 252 | const profiles = await coinbaseLibObject.getProfiles(); 253 | 254 | for (let i = 0; i < profiles.length; ++i) { 255 | if (profiles[i].name === depositProfileName) { 256 | accountObject.depositProfileID = profiles[i].id; 257 | } else if (profiles[i].name === tradingProfileName) { 258 | accountObject.tradeProfileID = profiles[i].id; 259 | } 260 | } 261 | 262 | if (!accountObject.depositProfileID) { 263 | throw new Error(`Could not find the deposit profile ID. Ensure that the depositProfileName: "${depositProfileName}" is spelt correctly.`) 264 | } 265 | if (!accountObject.tradeProfileID) { 266 | throw new Error(`Could not find the trade profile ID. Ensure that the tradingProfileName: "${tradingProfileName}" is spelt correctly.`) 267 | } 268 | 269 | return accountObject; 270 | } catch (err) { 271 | const message = "Error occured in getAccountIDs method."; 272 | const errorMsg = new Error(err); 273 | logger.error({ message, errorMsg, err }); 274 | throw err; 275 | } 276 | } 277 | 278 | /** 279 | * Gets information about the product being traded that the bot can use to determine how 280 | * accurate the size and quote values for the order needs to be. This method parses the base and quote increment 281 | * strings in order to determine to what precision the size and price parameters need to be when placing an order. 282 | * 283 | * @param {object} productInfo This object gets updated directly 284 | */ 285 | async function getProductInfo(productInfo) { 286 | try { 287 | let quoteIncrementRoundValue = 0; 288 | let baseIncrementRoundValue = 0; 289 | let productPairData; 290 | 291 | const products = await authedClient.getProducts(); 292 | 293 | for (let i = 0; i < products.length; ++i) { 294 | if (products[i].id === productInfo.productPair) { 295 | productPairData = products[i]; 296 | } 297 | } 298 | 299 | if (productPairData === undefined) { 300 | throw new Error(`Error, could not find a valid matching product pair for "${productInfo.productPair}". Verify the product names is correct/exists.`); 301 | } 302 | 303 | for (let i = 2; i < productPairData.quote_increment.length; ++i) { 304 | if (productPairData.quote_increment[i] === "1") { 305 | quoteIncrementRoundValue++; 306 | break; 307 | } else { 308 | quoteIncrementRoundValue++; 309 | } 310 | } 311 | 312 | if (productPairData.base_increment[0] !== "1") { 313 | for (let i = 2; i < productPairData.base_increment.length; ++i) { 314 | if (productPairData.base_increment[i] === "1") { 315 | baseIncrementRoundValue++; 316 | break; 317 | } else { 318 | baseIncrementRoundValue++; 319 | } 320 | } 321 | } 322 | 323 | productInfo.quoteIncrementRoundValue = Number(quoteIncrementRoundValue); 324 | productInfo.baseIncrementRoundValue = Number(baseIncrementRoundValue); 325 | } catch (err) { 326 | const message = "Error occurred in getProductInfo method."; 327 | const errorMsg = new Error(err); 328 | logger.error({ message, errorMsg, err }); 329 | throw err; 330 | } 331 | } 332 | 333 | /** 334 | * Retrieves the current maker and taker fees and returns the highest one as a number 335 | * 336 | * @param {number} highestFee The highest fee between the taker and maker fee 337 | */ 338 | async function returnHighestFee() { 339 | try { 340 | const feeResult = await coinbaseLibObject.getFees(); 341 | 342 | let makerFee = parseFloat(feeResult.maker_fee_rate); 343 | let takerFee = parseFloat(feeResult.taker_fee_rate); 344 | 345 | if (makerFee > takerFee) { 346 | return makerFee; 347 | } else { 348 | return takerFee; 349 | } 350 | } 351 | catch (err) { 352 | const message = "Error occurred in getFees method."; 353 | const errorMsg = new Error(err); 354 | logger.error({ message, errorMsg, err }); 355 | throw err; 356 | } 357 | } 358 | 359 | /** 360 | * This method is the entry point of the momentum strategy. It does some first time initialization then begins an infinite loop. 361 | * The loop checks the position info to decide if the bot needs to try and buy or sell, it also checks if there's an available 362 | * balance to be traded with. Then it calls gainPosition or losePosition appropiately and waits for them to finish and repeats. 363 | */ 364 | async function momentumStrategyStart() { 365 | try { 366 | logger.info(`Configuration:\napiURI: ${apiURI}\nwebsocketURI: ${websocketURI}\nsellPositionDelta: ${sellPositionDelta}\nbuyPositionDelta: ${buyPositionDelta}\norderPriceDelta: ${orderPriceDelta}\nbaseCurrencyName: ${baseCurrencyName}\nquoteCurrencyName: ${quoteCurrencyName}\ntradingProfileName: ${tradingProfileName}\ndepositProfileName: ${depositProfileName}\ndepositingEnabled: ${depositingEnabled}\ndepositingAmount: ${depositingAmount}\nbalanceMinimum: ${balanceMinimum}`); 367 | 368 | let accountIDs = {}; 369 | let lastPeakPrice; 370 | let lastValleyPrice; 371 | let highestFee = await returnHighestFee(); 372 | 373 | const tradingConfig = { 374 | sellPositionDelta, 375 | buyPositionDelta, 376 | orderPriceDelta, 377 | highestFee 378 | }; 379 | 380 | const depositConfig = { 381 | depositingEnabled, 382 | depositingAmount 383 | }; 384 | 385 | const productInfo = { 386 | baseCurrency: baseCurrencyName, 387 | quoteCurrency: quoteCurrencyName, 388 | productPair: baseCurrencyName + "-" + quoteCurrencyName 389 | }; 390 | 391 | let positionInfo; 392 | 393 | //Check for an existing positionData file to start the bot with: 394 | try { 395 | //read positionData file: 396 | let rawFileData = fileSystem.readFileSync("positionData.json"); 397 | positionInfo = JSON.parse(rawFileData); 398 | logger.info("Found positionData.json file, starting with position data. Position data: " + JSON.stringify(positionInfo)); 399 | } catch (err) { 400 | if (err.code === "ENOENT") { 401 | logger.info("No positionData file found, starting with no existing position."); 402 | } else { 403 | const message = "Error, failed to read file for a reason other than it doesn't exist. Continuing as normal but positionDataTracking might not work correctly."; 404 | const errorMsg = new Error(err); 405 | logger.error({ message, errorMsg, err }); 406 | } 407 | 408 | positionInfo = { 409 | positionExists: false, 410 | }; 411 | } 412 | 413 | //Retrieve product information: 414 | await getProductInfo(productInfo); 415 | logger.info(productInfo); 416 | 417 | //Retrieve account IDs: 418 | accountIDs = await getAccountIDs(productInfo); 419 | logger.info(accountIDs) 420 | 421 | //activate websocket for price data: 422 | listenForPriceUpdates(productInfo.productPair); 423 | 424 | while (currentPrice == null) { 425 | await sleep(1000); //Get a price before starting 426 | } 427 | 428 | logger.info(`Starting price of ${productInfo.baseCurrency} in ${productInfo.quoteCurrency} is: ${currentPrice}`); 429 | 430 | // eslint-disable-next-line no-constant-condition 431 | while (true) { 432 | if (positionInfo.positionExists) { 433 | tradingConfig.highestFee = await returnHighestFee(); 434 | await sleep(1000); 435 | const baseCurrencyAccount = await authedClient.getAccount(accountIDs.baseCurrencyAccountID); //Grab account information to view balance 436 | 437 | if (baseCurrencyAccount.available > 0) { 438 | logger.info("Entering lose position with: " + baseCurrencyAccount.available + " " + productInfo.baseCurrency); 439 | 440 | lastPeakPrice = currentPrice; 441 | lastValleyPrice = currentPrice; 442 | 443 | //Begin trying to sell position: 444 | await losePosition(parseFloat(baseCurrencyAccount.available), lastPeakPrice, lastValleyPrice, accountIDs, positionInfo, productInfo, depositConfig, tradingConfig); 445 | } else { 446 | throw new Error(`Error, there is no ${productInfo.baseCurrency} balance available for use. Terminating program.`); 447 | } 448 | } else { 449 | tradingConfig.highestFee = await returnHighestFee(); 450 | await sleep(1000); 451 | const quoteCurrencyAccount = await authedClient.getAccount(accountIDs.quoteCurrencyAccountID); //Grab account information to view balance 452 | const availableBalance = parseFloat(quoteCurrencyAccount.available); 453 | 454 | if (availableBalance > 0) { 455 | const tradeBalance = availableBalance - balanceMinimum; //Subtract this dollar amount so that there is room for rounding errors 456 | 457 | logger.info("Entering gain position with: " + tradeBalance + " " + productInfo.quoteCurrency); 458 | 459 | lastPeakPrice = currentPrice; 460 | lastValleyPrice = currentPrice; 461 | 462 | //Begin trying to buy a position: 463 | await gainPosition(tradeBalance, lastPeakPrice, lastValleyPrice, positionInfo, productInfo, tradingConfig); 464 | } else { 465 | throw new Error(`Error, there is no ${productInfo.quoteCurrency} balance available for use. Terminating program.`); 466 | } 467 | } 468 | } 469 | } catch (err) { 470 | const message = "Error occurred in bot, shutting down. Check the logs for more information."; 471 | const errorMsg = new Error(err); 472 | logger.error({ message, errorMsg, err }); 473 | process.exit(1); 474 | } 475 | } 476 | 477 | module.exports = momentumStrategyStart; 478 | -------------------------------------------------------------------------------- /strategies/momentumTradingWithStopLoss/momentumTradingWithStopLoss.js: -------------------------------------------------------------------------------- 1 | const CoinbasePro = require("coinbase-pro"); 2 | require('dotenv').config() 3 | const { buyPosition, sellPosition } = require("../../buyAndSell"); 4 | const coinbaseProLib = require("../../coinbaseProLibrary"); 5 | const pino = require("pino"); 6 | const logger = pino({ level: process.env.LOG_LEVEL || "info" }); 7 | const fileSystem = require("fs"); 8 | 9 | const key = `${process.env.API_KEY}`; 10 | const secret = `${process.env.API_SECRET}`; 11 | const passphrase = `${process.env.API_PASSPHRASE}`; 12 | 13 | //******************** Setup these value configurations before running the program ****************************************** 14 | 15 | //Determines the enviornment, add TRADING_ENV=real to use the real enviornment otherwise defaults to the sandbox: 16 | const apiURI = process.env.TRADING_ENV === "real" ? "https://api.pro.coinbase.com" : "https://api-public.sandbox.pro.coinbase.com"; 17 | const websocketURI = process.env.TRADING_ENV === "real" ? "wss://ws-feed.pro.coinbase.com" : "wss://ws-feed-public.sandbox.pro.coinbase.com"; 18 | 19 | //Trading config: 20 | //Global constants, consider tuning these values to optimize the bot's trading: 21 | const sellPositionDelta = Number(process.env.SELL_POSITION_DELTA) || .02; //The amount of change between peak and valley to trigger a sell off 22 | const buyPositionDelta = Number(process.env.BUY_POSITION_DELTA) || .015; //The amount of change between the valley and peak price to trigger a buy in 23 | const orderPriceDelta = Number(process.env.ORDER_PRICE_DELTA) || .001; //The amount of extra room to give the sell/buy orders to go through 24 | const stopLossDelta = Number(process.env.STOP_LOSS_DELTA) || .11; //The percent of loss allowed before selling and buying a lower position. 25 | 26 | //Currency config: 27 | //The pieces of the product pair, this is the two halves of coinbase product pair (examples of product pairs: BTC-USD, DASH-BTC, ETH-USDC). For BTC-USD the base currency is BTC and the quote currency is USD 28 | const baseCurrencyName = process.env.BASE_CURRENCY_NAME || "BTC"; 29 | const quoteCurrencyName = process.env.QUOTE_CURRENCY_NAME || "USD"; 30 | 31 | //Profile config: 32 | //Coinbase portfolios (profiles): 33 | const tradingProfileName = process.env.TRADING_PROFILE_NAME || "BTC trader"; //This is the name of the profile you want the bot to trade in 34 | const depositProfileName = process.env.DEPOSIT_PROFILE_NAME || "default"; //This is the name of the profile you want to deposit some profits to 35 | 36 | //Deposit config: 37 | const depositingEnabled = process.env.DEPOSITING_ENABLED !== "false"; //Choose whether or not you want you want to deposit a cut of the profits (Options: true/false) 38 | const depositingAmount = Number(process.env.DEPOSITING_AMOUNT) || 0.5; //Enter the amount of profit you want deposited (Options: choose a percent between 1 and 100 in decimal form I.E. .5 = 50%) 39 | 40 | // Due to rounding errors the buy order may not have enough funds to execute the order. This is the minimum funds amount in dollars that 41 | // will be left in usd account to avoid this error. Default = 6 cents (.06). 42 | const balanceMinimum = Number(process.env.BALANCE_MINIMUM) || .06; 43 | 44 | //*************************************************************************************************************************** 45 | 46 | //authedClient used to the API calls supported by the coinbase pro api node library 47 | let authedClient = new CoinbasePro.AuthenticatedClient( 48 | key, 49 | secret, 50 | passphrase, 51 | apiURI 52 | ); 53 | 54 | //Custom coinbase library used for making the calls not supported by the coinbase pro api node library 55 | const coinbaseLibObject = new coinbaseProLib(key, secret, passphrase, apiURI); 56 | 57 | //Global variable tracks the currentPrice. Updated by the websocket 58 | let currentPrice; 59 | 60 | /** 61 | * Makes the program sleep to avoid hitting API limits and let the websocket update 62 | * 63 | * @param {number} ms -> the number of milliseconds to wait 64 | */ 65 | function sleep(ms) { 66 | return new Promise((resolve) => { 67 | setTimeout(resolve, ms); 68 | }); 69 | } 70 | 71 | /** 72 | * Creates the websocket object and turns it on to update the currentPrice 73 | * 74 | * @param {string} productPair 75 | */ 76 | function listenForPriceUpdates(productPair) { 77 | if (productPair == null) { 78 | throw new Error("Error in listenForPriceUpdates method. ProductPair is null!"); 79 | } 80 | 81 | // The websocket client provides price updates on the product, refer to the docs for more information 82 | const websocket = new CoinbasePro.WebsocketClient( 83 | [productPair], 84 | websocketURI, 85 | { 86 | key, 87 | secret, 88 | passphrase, 89 | }, 90 | { channels: ["ticker"] } 91 | ); 92 | 93 | //turn on the websocket for errors 94 | websocket.on("error", function (err) { 95 | const message = "Error occurred in the websocket."; 96 | const errorMsg = new Error(err); 97 | logger.error({ message, errorMsg, err }); 98 | listenForPriceUpdates(productPair); 99 | }); 100 | 101 | //Turn on the websocket for closes to restart it 102 | websocket.on("close", function () { 103 | logger.debug("WebSocket closed, restarting..."); 104 | listenForPriceUpdates(productPair); 105 | }); 106 | 107 | //Turn on the websocket for messages 108 | websocket.on("message", function (data) { 109 | if (data.type === "ticker") { 110 | if (currentPrice !== data.price) { 111 | currentPrice = parseFloat(data.price); 112 | logger.debug("Ticker price: " + currentPrice); 113 | } 114 | } 115 | }); 116 | } 117 | 118 | /** 119 | * Loops forever until the conditions are right to attempt to sell the position. Every loop sleeps to let the currentPrice update 120 | * then updates the lastPeak/lastValley price as appropriate, if the price hits a new valley price it will check if the conditions are 121 | * met to sell the position and call the method if appropriate. 122 | * 123 | * @param {number} balance The amount of currency being traded with 124 | * @param {number} lastPeakPrice Tracks the price highs 125 | * @param {number} lastValleyPrice Tracks the price lows 126 | * @param {Object} accountIds The coinbase account ID associated with the API key used for storing a chunk of the profits in coinbase 127 | * @param {Object} positionInfo Contains 3 fields, positionExists (bool), positionAcquiredPrice (number), and positionAcquiredCost(number) 128 | * @param {Object} productInfo Contains information about the quote/base increment for the product pair 129 | * @param {Object} depositConfig Contains information about whether to do a deposit and for how much after a sell 130 | * @param {Object} tradingConfig Contains information about the fees and deltas 131 | */ 132 | async function losePosition(balance, lastPeakPrice, lastValleyPrice, accountIds, positionInfo, productInfo, depositConfig, tradingConfig) { 133 | try { 134 | while (positionInfo.positionExists === true) { 135 | await sleep(250); //Let price update 136 | 137 | if (lastPeakPrice < currentPrice) { 138 | //New peak hit, reset values 139 | lastPeakPrice = currentPrice; 140 | lastValleyPrice = currentPrice; 141 | 142 | logger.debug(`Sell Position, LPP: ${lastPeakPrice}`); 143 | } else if (lastValleyPrice > currentPrice) { 144 | //New valley hit, track valley and check sell conditions 145 | lastValleyPrice = currentPrice; 146 | 147 | const target = lastPeakPrice - (lastPeakPrice * sellPositionDelta); 148 | const lowestSellPrice = lastValleyPrice - (lastValleyPrice * orderPriceDelta); 149 | const receivedValue = (lowestSellPrice * balance) - ((lowestSellPrice * balance) * tradingConfig.highestFee); 150 | const stopLossPrice = positionInfo.positionAcquiredPrice - (positionInfo.positionAcquiredPrice * stopLossDelta); 151 | 152 | logger.debug(`Sell Position, LVP: ${lastValleyPrice} needs to be less than or equal to ${target} to sell and the receivedValue: ${receivedValue} needs to be greater than the positionAcquiredCost: ${positionInfo.positionAcquiredCost} unless the LVP: ${lastValleyPrice} is less than the stop loss price: ${stopLossPrice}`); 153 | 154 | if ((lastValleyPrice <= target) && (receivedValue > positionInfo.positionAcquiredCost)) { 155 | logger.info("Attempting to sell position..."); 156 | 157 | //Create a new authenticated client to prevent it from expiring or hitting API limits 158 | authedClient = new CoinbasePro.AuthenticatedClient( 159 | key, 160 | secret, 161 | passphrase, 162 | apiURI 163 | ); 164 | 165 | await sellPosition(balance, accountIds, positionInfo, lastValleyPrice, authedClient, coinbaseLibObject, productInfo, depositConfig, tradingConfig); 166 | } else if (lastValleyPrice <= stopLossPrice) { 167 | logger.info("Attempting to sell position at a loss..."); 168 | 169 | //Create a new authenticated client to prevent it from expiring or hitting API limits 170 | authedClient = new CoinbasePro.AuthenticatedClient( 171 | key, 172 | secret, 173 | passphrase, 174 | apiURI 175 | ); 176 | 177 | await sellPosition(balance, accountIds, positionInfo, lastValleyPrice, authedClient, coinbaseLibObject, productInfo, depositConfig, tradingConfig); 178 | } 179 | } 180 | } 181 | } catch (err) { 182 | const message = "Error occurred in losePosition method."; 183 | const errorMsg = new Error(err); 184 | logger.error({ message, errorMsg, err }); 185 | throw err; 186 | } 187 | } 188 | 189 | /** 190 | * Loops forever until the conditions are right to attempt to buy a position. Every loop sleeps to let the currentPrice update 191 | * then updates the lastPeak/lastValley price as appropriate, if the price hits a new peak price it will check if the conditions are 192 | * met to buy the position and call the method if appropriate. 193 | * 194 | * @param {number} balance The amount of currency being traded with 195 | * @param {number} lastPeakPrice Tracks the price highs 196 | * @param {number} lastValleyPrice Tracks the price lows 197 | * @param {Object} positionInfo Contains 3 fields, positionExists (bool), positionAcquiredPrice (number), and positionAcquiredCost(number) 198 | * @param {Object} productInfo Contains information about the quote/base increment for the product pair 199 | * @param {Object} tradingConfig Contains information about the fees and deltas 200 | */ 201 | async function gainPosition(balance, lastPeakPrice, lastValleyPrice, positionInfo, productInfo, tradingConfig) { 202 | try { 203 | while (positionInfo.positionExists === false) { 204 | await sleep(250); //Let price update 205 | 206 | if (lastPeakPrice < currentPrice) { 207 | //New peak hit, track peak price and check buy conditions 208 | lastPeakPrice = currentPrice; 209 | 210 | const target = lastValleyPrice + (lastValleyPrice * buyPositionDelta); 211 | 212 | logger.debug(`Buy Position, LPP: ${lastPeakPrice} needs to be greater than or equal to ${target} to buy`); 213 | 214 | if (lastPeakPrice >= target) { 215 | logger.info("Attempting to buy position..."); 216 | 217 | //Create a new authenticated client to prevent it from expiring or hitting API limits 218 | authedClient = new CoinbasePro.AuthenticatedClient( 219 | key, 220 | secret, 221 | passphrase, 222 | apiURI 223 | ); 224 | 225 | await buyPosition(balance, positionInfo, lastPeakPrice, authedClient, productInfo, tradingConfig); 226 | } 227 | } else if (lastValleyPrice > currentPrice) { 228 | //New valley hit, reset values 229 | 230 | lastPeakPrice = currentPrice; 231 | lastValleyPrice = currentPrice; 232 | 233 | logger.debug(`Buy Position, LVP: ${lastValleyPrice}`); 234 | } 235 | } 236 | } catch (err) { 237 | const message = "Error occurred in gainPosition method."; 238 | const errorMsg = new Error(err); 239 | logger.error({ message, errorMsg, err }); 240 | throw err; 241 | } 242 | } 243 | 244 | /** 245 | * Acquires some account ID information to be used for storing and retrieving information and depositing funds after a sell. 246 | * 247 | * @param {Object} productInfo productInfo contains the base and quote currencies being traded with needed to grab the correct account IDs 248 | * @return {Object} accountObject contains the needed account IDs and profile IDs needed for checking balances and making transfers 249 | */ 250 | async function getAccountIDs(productInfo) { 251 | try { 252 | let accountObject = {}; 253 | 254 | //Gets the account IDs for the product pairs in the portfolio 255 | const accounts = await authedClient.getAccounts(); 256 | 257 | for (let i = 0; i < accounts.length; ++i) { 258 | if (accounts[i].currency === productInfo.baseCurrency) { 259 | accountObject.baseCurrencyAccountID = accounts[i].id; 260 | } else if (accounts[i].currency === productInfo.quoteCurrency) { 261 | accountObject.quoteCurrencyAccountID = accounts[i].id; 262 | } 263 | } 264 | 265 | //Gets all the profiles belonging to the user and matches the deposit and trading profile IDs 266 | const profiles = await coinbaseLibObject.getProfiles(); 267 | 268 | for (let i = 0; i < profiles.length; ++i) { 269 | if (profiles[i].name === depositProfileName) { 270 | accountObject.depositProfileID = profiles[i].id; 271 | } else if (profiles[i].name === tradingProfileName) { 272 | accountObject.tradeProfileID = profiles[i].id; 273 | } 274 | } 275 | 276 | if (!accountObject.depositProfileID) { 277 | throw new Error(`Could not find the deposit profile ID. Ensure that the depositProfileName: "${depositProfileName}" is spelt correctly.`) 278 | } 279 | if (!accountObject.tradeProfileID) { 280 | throw new Error(`Could not find the trade profile ID. Ensure that the tradingProfileName: "${tradingProfileName}" is spelt correctly.`) 281 | } 282 | 283 | return accountObject; 284 | } catch (err) { 285 | const message = "Error occured in getAccountIDs method."; 286 | const errorMsg = new Error(err); 287 | logger.error({ message, errorMsg, err }); 288 | throw err; 289 | } 290 | } 291 | 292 | /** 293 | * Gets information about the product being traded that the bot can use to determine how 294 | * accurate the size and quote values for the order needs to be. This method parses the base and quote increment 295 | * strings in order to determine to what precision the size and price parameters need to be when placing an order. 296 | * 297 | * @param {object} productInfo This object gets updated directly 298 | */ 299 | async function getProductInfo(productInfo) { 300 | try { 301 | let quoteIncrementRoundValue = 0; 302 | let baseIncrementRoundValue = 0; 303 | let productPairData; 304 | 305 | const products = await authedClient.getProducts(); 306 | 307 | for (let i = 0; i < products.length; ++i) { 308 | if (products[i].id === productInfo.productPair) { 309 | productPairData = products[i]; 310 | } 311 | } 312 | 313 | if (productPairData === undefined) { 314 | throw new Error(`Error, could not find a valid matching product pair for "${productInfo.productPair}". Verify the product names is correct/exists.`); 315 | } 316 | 317 | for (let i = 2; i < productPairData.quote_increment.length; ++i) { 318 | if (productPairData.quote_increment[i] === "1") { 319 | quoteIncrementRoundValue++; 320 | break; 321 | } else { 322 | quoteIncrementRoundValue++; 323 | } 324 | } 325 | 326 | if (productPairData.base_increment[0] !== "1") { 327 | for (let i = 2; i < productPairData.base_increment.length; ++i) { 328 | if (productPairData.base_increment[i] === "1") { 329 | baseIncrementRoundValue++; 330 | break; 331 | } else { 332 | baseIncrementRoundValue++; 333 | } 334 | } 335 | } 336 | 337 | productInfo.quoteIncrementRoundValue = Number(quoteIncrementRoundValue); 338 | productInfo.baseIncrementRoundValue = Number(baseIncrementRoundValue); 339 | } catch (err) { 340 | const message = "Error occurred in getProductInfo method."; 341 | const errorMsg = new Error(err); 342 | logger.error({ message, errorMsg, err }); 343 | throw err; 344 | } 345 | } 346 | 347 | /** 348 | * Retrieves the current maker and taker fees and returns the highest one as a number 349 | * 350 | * @param {number} highestFee The highest fee between the taker and maker fee 351 | */ 352 | async function returnHighestFee() { 353 | try { 354 | const feeResult = await coinbaseLibObject.getFees(); 355 | 356 | let makerFee = parseFloat(feeResult.maker_fee_rate); 357 | let takerFee = parseFloat(feeResult.taker_fee_rate); 358 | 359 | if (makerFee > takerFee) { 360 | return makerFee; 361 | } else { 362 | return takerFee; 363 | } 364 | } 365 | catch (err) { 366 | const message = "Error occurred in getFees method."; 367 | const errorMsg = new Error(err); 368 | logger.error({ message, errorMsg, err }); 369 | throw err; 370 | } 371 | } 372 | 373 | /** 374 | * This method is the entry point of the momentum strategy. It does some first time initialization then begins an infinite loop. 375 | * The loop checks the position info to decide if the bot needs to try and buy or sell, it also checks if there's an available 376 | * balance to be traded with. Then it calls gainPosition or losePosition appropiately and waits for them to finish and repeats. 377 | */ 378 | async function momentumStrategyStart() { 379 | try { 380 | logger.info(`Configuration:\napiURI: ${apiURI}\nwebsocketURI: ${websocketURI}\nsellPositionDelta: ${sellPositionDelta}\nbuyPositionDelta: ${buyPositionDelta}\norderPriceDelta: ${orderPriceDelta}\nbaseCurrencyName: ${baseCurrencyName}\nquoteCurrencyName: ${quoteCurrencyName}\ntradingProfileName: ${tradingProfileName}\ndepositProfileName: ${depositProfileName}\ndepositingEnabled: ${depositingEnabled}\ndepositingAmount: ${depositingAmount}\nbalanceMinimum: ${balanceMinimum}`); 381 | 382 | let accountIDs = {}; 383 | let lastPeakPrice; 384 | let lastValleyPrice; 385 | let highestFee = await returnHighestFee(); 386 | 387 | const tradingConfig = { 388 | sellPositionDelta, 389 | buyPositionDelta, 390 | orderPriceDelta, 391 | highestFee 392 | }; 393 | 394 | const depositConfig = { 395 | depositingEnabled, 396 | depositingAmount 397 | }; 398 | 399 | const productInfo = { 400 | baseCurrency: baseCurrencyName, 401 | quoteCurrency: quoteCurrencyName, 402 | productPair: baseCurrencyName + "-" + quoteCurrencyName 403 | }; 404 | 405 | let positionInfo; 406 | 407 | //Check for an existing positionData file to start the bot with: 408 | try { 409 | //read positionData file: 410 | let rawFileData = fileSystem.readFileSync("positionData.json"); 411 | positionInfo = JSON.parse(rawFileData); 412 | logger.info("Found positionData.json file, starting with position data. Position data: " + JSON.stringify(positionInfo)); 413 | } catch (err) { 414 | if (err.code === "ENOENT") { 415 | logger.info("No positionData file found, starting with no existing position."); 416 | } else { 417 | const message = "Error, failed to read file for a reason other than it doesn't exist. Continuing as normal but positionDataTracking might not work correctly."; 418 | const errorMsg = new Error(err); 419 | logger.error({ message, errorMsg, err }); 420 | } 421 | 422 | positionInfo = { 423 | positionExists: false, 424 | }; 425 | } 426 | 427 | //Retrieve product information: 428 | await getProductInfo(productInfo); 429 | logger.info(productInfo); 430 | 431 | //Retrieve account IDs: 432 | accountIDs = await getAccountIDs(productInfo); 433 | logger.info(accountIDs) 434 | 435 | //activate websocket for price data: 436 | listenForPriceUpdates(productInfo.productPair); 437 | 438 | while (currentPrice == null) { 439 | await sleep(1000); //Get a price before starting 440 | } 441 | 442 | logger.info(`Starting price of ${productInfo.baseCurrency} in ${productInfo.quoteCurrency} is: ${currentPrice}`); 443 | 444 | // eslint-disable-next-line no-constant-condition 445 | while (true) { 446 | if (positionInfo.positionExists) { 447 | tradingConfig.highestFee = await returnHighestFee(); 448 | await sleep(1000); 449 | const baseCurrencyAccount = await authedClient.getAccount(accountIDs.baseCurrencyAccountID); //Grab account information to view balance 450 | 451 | if (baseCurrencyAccount.available > 0) { 452 | logger.info("Entering lose position with: " + baseCurrencyAccount.available + " " + productInfo.baseCurrency); 453 | 454 | lastPeakPrice = currentPrice; 455 | lastValleyPrice = currentPrice; 456 | 457 | //Begin trying to sell position: 458 | await losePosition(parseFloat(baseCurrencyAccount.available), lastPeakPrice, lastValleyPrice, accountIDs, positionInfo, productInfo, depositConfig, tradingConfig); 459 | } else { 460 | throw new Error(`Error, there is no ${productInfo.baseCurrency} balance available for use. Terminating program.`); 461 | } 462 | } else { 463 | tradingConfig.highestFee = await returnHighestFee(); 464 | await sleep(1000); 465 | const quoteCurrencyAccount = await authedClient.getAccount(accountIDs.quoteCurrencyAccountID); //Grab account information to view balance 466 | const availableBalance = parseFloat(quoteCurrencyAccount.available); 467 | 468 | if (availableBalance > 0) { 469 | const tradeBalance = availableBalance - balanceMinimum; //Subtract this dollar amount so that there is room for rounding errors 470 | 471 | logger.info("Entering gain position with: " + tradeBalance + " " + productInfo.quoteCurrency); 472 | 473 | lastPeakPrice = currentPrice; 474 | lastValleyPrice = currentPrice; 475 | 476 | //Begin trying to buy a position: 477 | await gainPosition(tradeBalance, lastPeakPrice, lastValleyPrice, positionInfo, productInfo, tradingConfig); 478 | } else { 479 | throw new Error(`Error, there is no ${productInfo.quoteCurrency} balance available for use. Terminating program.`); 480 | } 481 | } 482 | } 483 | } catch (err) { 484 | const message = "Error occurred in bot, shutting down. Check the logs for more information."; 485 | const errorMsg = new Error(err); 486 | logger.error({ message, errorMsg, err }); 487 | process.exit(1); 488 | } 489 | } 490 | 491 | module.exports = momentumStrategyStart; 492 | --------------------------------------------------------------------------------