Skip to content

Commit

Permalink
Merge pull request #16 from LeviathanLevi/V1.5
Browse files Browse the repository at this point in the history
Merge branch V1.5 into master
  • Loading branch information
LeviathanLevi authored Apr 17, 2021
2 parents e682ea4 + d6f52c2 commit ad63bc0
Show file tree
Hide file tree
Showing 8 changed files with 964 additions and 13 deletions.
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# CrypFinder Bot
## Version 1.4
## Version 1.5

## CrypFinder Summary:
CrypFinder is a Coinbase Pro API trading bot that currently implements a basic 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 supported, only USD markets (USD to coins).
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).

The currently implemented 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 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.

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.

Expand Down Expand Up @@ -54,8 +54,8 @@ Notice that the position acquired cost and price fields still exist in the file
## Running the program out of sandbox:
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. You can run this program on your own machine or consider using a server such as an AWS EC2 instance with an EIP (you need to whitelist the API IP). AWS EC2 offers a free tier instance for a year that works well for hosting.

## Momentum trading strategy analyzer:
The momentumTradingAnalyzer is a way to run data against the momentum trading 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.
## Momentum and reverse momentum trading strategy analyzer:
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.

## Helpful links:
[Coinbase Pro](https://pro.coinbase.com/trade/BTC-USD)
Expand All @@ -67,10 +67,9 @@ The momentumTradingAnalyzer is a way to run data against the momentum trading bo
[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)

## Roadmap:
- Implement a CLI (command line interface) to control the bot. This would make it so that users won't have to edit the code directly to configure and run the bot.

### Possible future goals:
- Add more strategies or make the current momentum strategy better. If making major changes to a current trading strategy, keep the old version and just add a new version of it to the same folder (momentumTradingV1, V2, etc).
- Implement a CLI (command line interface) to control the bot. This would make it so that users won't have to edit the code directly to configure and run the bot.

## Interested in the project?:
Consider getting involved. Free to contact the creator on GitHub ([Levi Leuthold](https://github.com/LeviathanLevi)) for information on how to get started! Checkout the product roadmap to see what features are currently planned for the future or add your own ideas.
Expand Down
21 changes: 16 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
/* eslint-disable no-unused-vars */

/*
* This is the entry point of program. Currently, there is just one strategy but this will be the place where
* a specific strategy would be selected to start the program with. In the future, this project could use
* a command line interface here for controlling it.
* This is the entry point of program. Select the strategy or analyzer(s)
*/
const momentumStrategyStart = require("./strategies/momentumTrading/momentumTrading");
const momentumStrategyAnalyzerStart = require("./strategies/momentumTrading/momentumTradingAnalyzer");
//Make sure to configure the momentumStrategy in ./strategies/momentumTrading/momentumTrading.js before launching

const reverseMomentumStrategyStart = require("./strategies/reverseMomentumTrading/reverseMomentumTrading");
const reverseMomentumStrategyAnalyzerStart = require("./strategies/reverseMomentumTrading/reverseMomentumTradingAnalyzer");


/*** Make sure to configure the momentumStrategy in ./strategies/momentumTrading/momentumTrading.js before launching ***/
//Launches the momentum strategy and starts the bot:
momentumStrategyStart();

//Launches the momentum strategy anaylzer for back testing:
//momentumStrategyAnalyzerStart();
//momentumStrategyAnalyzerStart();



/*** Make sure to configure the momentumStrategy in ./strategies/momentumTrading/momentumTrading.js before launching ***/
//Launches the reverse momentum strategy and starts the bot:
//reverseMomentumStrategyStart();

//Launches the reverse momentum strategy anaylzer for back testing:
//reverseMomentumStrategyAnalyzerStart();
2 changes: 1 addition & 1 deletion strategies/momentumTrading/momentumTrading.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const websocketURI = "wss://ws-feed-public.sandbox.pro.coinbase.com";
//Trading config:
//Global constants, consider tuning these values to optimize the bot's trading:
const sellPositionDelta = .02; //The amount of change between peak and valley to trigger a sell off
const buyPositionDelta = .015; //The amount of change between the peak and valley price to trigger a buy in
const buyPositionDelta = .015; //The amount of change between the valley and peak price to trigger a buy in
const orderPriceDelta = .001; //The amount of extra room to give the sell/buy orders to go through

//Currency config:
Expand Down
4 changes: 4 additions & 0 deletions strategies/momentumTrading/momentumTrading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Momentum Trading Strategy

## Summary:
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 boy 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.
224 changes: 224 additions & 0 deletions strategies/reverseMomentumTrading/buyAndSell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* This module contains methods to buy a position and sell a position. It uses a limit order then loops checking the order
* status until the order either completes, OR after 1 minute it will cancel the order.
*/
const pino = require("pino");
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
const fileSystem = require("fs");

/**
* Halts the program from running temporarily to prevent it from hitting API call limits
*
* @param {number} ms -> the number of miliseconds to wait
*/
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

/**
* 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
* 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
* in the alloted time span (1 minute) then the method cancels the order and throws an exception.
*
* @param {Number} balance
* @param {Object} accountIds
* @param {Object} positionInfo
* @param {Number} currentPrice
* @param {Object} authedClient
* @param {Object} coinbaseLibObject
* @param {Object} productInfo
* @param {Object} depositConfig
* @param {Object} tradingConfig
*/
async function sellPosition(balance, accountIds, positionInfo, currentPrice, authedClient, coinbaseLibObject, productInfo, depositConfig, tradingConfig) {
try {
const priceToSell = (currentPrice - (currentPrice * tradingConfig.orderPriceDelta)).toFixed(productInfo.quoteIncrementRoundValue);

let orderSize;
if (productInfo.baseIncrementRoundValue === 0) {
orderSize = Math.trunc(balance);
} else {
orderSize = (balance).toFixed(productInfo.baseIncrementRoundValue);
}

const orderParams = {
side: "sell",
price: priceToSell,
size: orderSize,
product_id: productInfo.productPair,
time_in_force: "FOK"
};

logger.info("Sell order params: " + JSON.stringify(orderParams));

//Place sell order
const order = await authedClient.placeOrder(orderParams);
logger.debug(order);
const orderID = order.id;

//Loop to wait for order to be filled:
for (let i = 0; i < 10 && positionInfo.positionExists === true; ++i) {
let orderDetails;
logger.debug("Checking sell order result...");
await sleep(6000); //wait 6 seconds
try {
orderDetails = await authedClient.getOrder(orderID); //Get latest order details
} catch(err) {
const message = "Error occured when attempting to get the order.";
const errorMsg = new Error(err);
logger.error({ message, errorMsg, err });
continue;
}
logger.debug(orderDetails);

if (orderDetails.status === "done") {
if (orderDetails.done_reason !== "filled") {
throw new Error("Sell order did not complete due to being filled? done_reason: " + orderDetails.done_reason);
} else {
positionInfo.positionExists = false;

//Update positionData file:
try {
const writeData = JSON.stringify(positionInfo);
fileSystem.writeFileSync("positionData.json", writeData);
} catch(err) {
const message = "Error, failed to write the positionInfo to the positionData file in sellPosition. Continuing as normal but but positionDataTracking might not work correctly.";
const errorMsg = new Error(err);
logger.error({ message, errorMsg, err });
}

let profit = parseFloat(orderDetails.executed_value) - parseFloat(orderDetails.fill_fees) - positionInfo.positionAcquiredCost;
logger.info("Profit: " + profit);

if (profit > 0) {
//Check deposit config:
if (depositConfig.depositingEnabled) {
const transferAmount = (profit * depositConfig.depositingAmount).toFixed(2);
const currency = productInfo.quoteCurrency;

//Transfer funds to depositProfileID
const transferResult = await coinbaseLibObject.profileTransfer(accountIds.tradeProfileID, accountIds.depositProfileID, currency, transferAmount);

logger.debug("transfer result: " + transferResult);
}
} else {
throw new Error("Sell was not profitable, terminating program. profit: " + profit);
}
}
}
}

//Check if order wasn't filled and needs cancelled:
if (positionInfo.positionExists === true) {
const cancelOrder = await authedClient.cancelOrder(orderID);
if (cancelOrder !== orderID) {
throw new Error("Attempted to cancel failed order but it did not work. cancelOrderReturn: " + cancelOrder + "orderID: " + orderID);
}
}

} catch (err) {
const message = "Error occured in sellPosition method.";
const errorMsg = new Error(err);
logger.error({ message, errorMsg, err });
}
}

/**
* 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
* 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
* order and throw an exception.
*
* @param {Number} balance
* @param {Object} positionInfo
* @param {Number} currentPrice
* @param {Object} authedClient
* @param {Object} productInfo
* @param {Object} tradingConfig
*/
async function buyPosition(balance, positionInfo, currentPrice, authedClient, productInfo, tradingConfig) {
try {
const amountToSpend = balance - (balance * tradingConfig.highestFee);
const priceToBuy = (currentPrice + (currentPrice * tradingConfig.orderPriceDelta)).toFixed(productInfo.quoteIncrementRoundValue);
let orderSize;

if (productInfo.baseIncrementRoundValue === 0) {
orderSize = Math.trunc(amountToSpend / priceToBuy);
} else {
orderSize = (amountToSpend / priceToBuy).toFixed(productInfo.baseIncrementRoundValue);
}

const orderParams = {
side: "buy",
price: priceToBuy,
size: orderSize,
product_id: productInfo.productPair,
time_in_force: "FOK"
};

logger.info("Buy order params: " + JSON.stringify(orderParams));

//Place buy order
const order = await authedClient.placeOrder(orderParams);
logger.debug(order);
const orderID = order.id;

//Loop to wait for order to be filled:
for (let i = 0; i < 10 && positionInfo.positionExists === false; ++i) {
let orderDetails;
logger.debug("Checking buy order result...");
await sleep(6000); //wait 6 seconds
try {
orderDetails = await authedClient.getOrder(orderID); //Get latest order details
} catch(err) {
const message = "Error occured when attempting to get the order.";
const errorMsg = new Error(err);
logger.error({ message, errorMsg, err });
continue;
}
logger.debug(orderDetails);

if (orderDetails.status === "done") {
if (orderDetails.done_reason !== "filled") {
throw new Error("Buy order did not complete due to being filled? done_reason: " + orderDetails.done_reason);
} else {
//Update position info
positionInfo.positionExists = true;
positionInfo.positionAcquiredPrice = parseFloat(orderDetails.executed_value) / parseFloat(orderDetails.filled_size);
positionInfo.positionAcquiredCost = parseFloat(orderDetails.executed_value) + parseFloat(orderDetails.fill_fees);

//Update positionData file:
try {
const writeData = JSON.stringify(positionInfo);
fileSystem.writeFileSync("positionData.json", writeData);
} catch(err) {
const message = "Error, failed to write the positionInfo to the positionData file in buyPosition. Continuing as normal but but positionDataTracking might not work correctly.";
const errorMsg = new Error(err);
logger.error({ message, errorMsg, err });
}

logger.info(positionInfo);
}
}
}

//Check if order wasn't filled and needs cancelled
if (positionInfo.positionExists === false) {
const cancelOrder = await authedClient.cancelOrder(orderID);
if (cancelOrder !== orderID) {
throw new Error("Attempted to cancel failed order but it did not work. cancelOrderReturn: " + cancelOrder + "orderID: " + orderID);
}
}

} catch (err) {
const message = "Error occured in buyPosition method.";
const errorMsg = new Error(err);
logger.error({ message, errorMsg, err });
}
}

module.exports = {
sellPosition,
buyPosition,
}
Loading

0 comments on commit ad63bc0

Please sign in to comment.