├── .gitignore ├── src ├── services │ ├── mailService.js │ └── rbhApiService.js ├── enums.js ├── main.js ├── utils.js ├── models │ └── Rule.js └── engine │ └── Engine.js ├── package.json ├── config └── .env └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | env.js 2 | config/env.js 3 | .idea 4 | -------------------------------------------------------------------------------- /src/services/mailService.js: -------------------------------------------------------------------------------- 1 | const { EMAIL_CONFIG } = require('../../config/env'); 2 | const { createTransport } = require('nodemailer'); 3 | 4 | class MailService { 5 | constructor() { 6 | this.transport = createTransport(EMAIL_CONFIG.transport); 7 | } 8 | 9 | send({ from, to, subject, text}) { 10 | const options = Object.assign({}, EMAIL_CONFIG.options, { from, to, subject, text}); 11 | return this.transport.sendMail(options); 12 | } 13 | } 14 | 15 | module.exports = new MailService(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-day-trader", 3 | "version": "1.0.1", 4 | "description": "Minimal trading engine written in NodeJs", 5 | "main": "server/src", 6 | "scripts": { 7 | "start": "node src/main.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jcvilap/artificial-trader.git" 12 | }, 13 | "author": "Julio Vila", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/jcvilap/artificial-trader/issues" 17 | }, 18 | "homepage": "artificialtrader.surge.sh", 19 | "dependencies": { 20 | "lodash": "^4.17.10", 21 | "moment": "^2.20.1", 22 | "mongoose": "^5.0.9", 23 | "nodemailer": "^4.6.7", 24 | "request": "^2.83.0", 25 | "request-promise-native": "^1.0.5", 26 | "uuid": "^3.3.2" 27 | }, 28 | "devDependencies": { 29 | "nodemon": "^1.14.11" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /config/.env: -------------------------------------------------------------------------------- 1 | /** 2 | * Note, after checking out the project, rename this file to env.js and fill in the blanks 3 | */ 4 | module.exports = { 5 | PORT: {{port}}, 6 | RBH_API_BASE: 'https://api.robinhood.com', 7 | NUMMUS_RH_API_BASE: 'https://nummus.robinhood.com', 8 | RH_CREDENTIALS: { 9 | username: {{robinhood_user_name}}, 10 | password: {{robinhood_password}}, 11 | client_id: {{robinhood_client_id}}, 12 | }, 13 | DB: `mongodb://{{user}}:{{pass}}@{{your_db_url}}`, 14 | EMAIL: { 15 | EMAIL_CONFIG: { 16 | transport: { 17 | enabled: true, 18 | service: 'e.g. iCloud or gmail', 19 | auth: { 20 | user: {{email}}, 21 | pass: {{password}} 22 | }, 23 | tls: { rejectUnauthorized: false } 24 | }, 25 | options: { 26 | from: {{sender}}, 27 | to: {{list_of_receivers}}, 28 | subject: 'Activity Recorded', 29 | text: '' 30 | } 31 | } 32 | } 33 | }; -------------------------------------------------------------------------------- /src/enums.js: -------------------------------------------------------------------------------- 1 | const OrderStatus = {FILLED: 'FILLED', PENDING: 'PENDING', REJECTED: 'REJECTED', CANCELLED: 'CANCELLED'}; 2 | const OrderType = {BUY: 'BUY', SELL: 'SELL'}; 3 | const TimeRange = {HOUR: 'HOUR', DAY: 'DAY', WEEK: 'WEEK', MONTH: 'MONTH', YEAR: 'YEAR'}; 4 | const ChannelType = { 5 | TICKER: 'ticker', // provides real-time price updates every time a match happens 6 | HEARTBEAT: 'heartbeat', // includes sequence and last trade ids that can be used to verify no messages were missed. 7 | LEVEL2: 'level2', // snapshot of the order book 8 | USER: 'user', // contains messages that include the authenticated user 9 | MATCHES: 'matches', // match messages 10 | FULL: 'full', // real-time updates on orders and trades 11 | }; 12 | const Granularity = { 13 | ONE_MINUTE: 60, 14 | FIVE_MINUTES: 300, 15 | FIFTEEN_MINUTES: 900, 16 | ONE_HOUR: 3600, 17 | SIX_HOURS: 21600, 18 | ONE_DAY: 86400 19 | }; 20 | module.exports = {OrderType, OrderStatus, TimeRange, ChannelType, Granularity}; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('http'); 2 | const mongoose = require('mongoose'); 3 | const { PORT, DB } = require('../config/env'); 4 | const Engine = require('./engine/Engine'); 5 | 6 | class App { 7 | constructor() { 8 | this.server = createServer(); 9 | this.engine = new Engine(); 10 | mongoose.connect(DB, { useNewUrlParser: true }); 11 | this.db = mongoose.connection; 12 | 13 | this.handleExit = this.handleExit.bind(this); 14 | this.registerEvents(); 15 | this.start(); 16 | } 17 | 18 | registerEvents() { 19 | process.on('SIGTERM', this.handleExit); 20 | this.db.on('error', (e) => console.error('connection error:', e)); 21 | this.db.once('open', () => console.log('Database connected')); 22 | } 23 | 24 | /** 25 | * After successfully listening on port, start the engine 26 | */ 27 | async start() { 28 | this.server.listen(PORT, () => { 29 | console.log('Listening to port:', PORT); 30 | this.engine.start(); 31 | }) 32 | } 33 | 34 | handleExit() { 35 | this.server.close(() => process.exit(0)); 36 | } 37 | } 38 | 39 | module.exports = new App(); -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | class Utils { 4 | static precisionRound(number, precision) { 5 | const factor = Math.pow(10, precision); 6 | return Math.round(number * factor) / factor; 7 | } 8 | 9 | /** 10 | * RSI(14) over one minute 11 | * Assumes passed data represents 1hr overall over 15 secs interval 12 | * @param historicals 13 | * @returns {number} 14 | */ 15 | static calculateRSI(historicals = []) { 16 | const data_points = []; 17 | let avgGain = 0; 18 | let aveLoss = 0; 19 | let bucketMinute = null; 20 | 21 | // Get last 15 mins worth of data 22 | for (let i = historicals.length - 1; i > (historicals.length - 1) - (15 * 4); i--) { 23 | const minute = moment(historicals[i].begins_at).minute(); 24 | if (bucketMinute !== minute) { 25 | data_points.push(historicals[i].close_price); 26 | bucketMinute = minute; 27 | } 28 | } 29 | // Calculate averages 30 | for (let i = 0; i < 14; i++) { 31 | const ch = data_points[i] - data_points[i + 1]; 32 | if (ch >= 0) { 33 | avgGain += ch; 34 | } else { 35 | aveLoss -= ch; 36 | } 37 | } 38 | avgGain /= 14; 39 | aveLoss /= 14; 40 | // Calculate RS 41 | const RS = avgGain / aveLoss; 42 | // Return RSI 43 | return 100 - (100 / (1 + RS)); 44 | } 45 | 46 | static calculateCurrencyAmount(quotePrice, balance, percentage) { 47 | const _quotePrice = Number(quotePrice); 48 | const _balance = Number(balance); 49 | const _percentage = Number(percentage); 50 | const amountToInvest = _balance * (_percentage / 100); 51 | const result = amountToInvest / _quotePrice; 52 | return result.toFixed(8).toString(); 53 | } 54 | 55 | static formatJSON(json, spaces = 2) { 56 | return JSON.stringify(json, null, spaces) 57 | } 58 | } 59 | 60 | module.exports = Utils; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypto Day-Trader 2 | This project is the result of many small failed attempts to build a true commission-free trading engine. It was possible for me to work on it after Robinhood released crypto-currencies support. Although coupled to Robinhood API, this code is easily extendable to any broker or API. 3 | 4 | 5 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/jcvilap/crypto-day-trader) 6 | 7 | ## Milestones 8 | #### Setup 9 | - [x] Set up a Node server and deploy it to Heroku 10 | - [x] Setup a Mongo instance in mLab and connect to it from Node 11 | 12 | #### Server 13 | - [x] Handle security based on Robinhood account 14 | - [x] Listen to changes on selected crypto-currency (for now only one currency is supported) 15 | - [x] Fetch user account info from Robinhood 16 | - [x] Define `Rule` class 17 | - [x] Fetch user `rules` from database 18 | - [x] Enable Buy/Sell actions 19 | - [x] Calculate and incorporate RSI analysis to Buy/Sell strategies 20 | - [x] Perform analysis based on `rules` attributes 21 | 22 | ### Rules 23 | I define a `Rule` as a single instance of multiple trading strategies to be used by the `Engine`. Rules can be defined by the user and will be stored in the Mongo instance 24 | 25 | ### License 26 | 27 | Copyright (c) 2018 Mauer Principles Inc 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 32 | 33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | 35 | -------------------------------------------------------------------------------- /src/models/Rule.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Utils = require('../utils'); 3 | 4 | const Rule = new mongoose.Schema({ 5 | /** 6 | * Product id 7 | * @example 'BTC' 8 | */ 9 | currency_code: String, 10 | /** 11 | * Currency PK 12 | */ 13 | currency_id: mongoose.Schema.ObjectId, 14 | /** 15 | * Price per bitcoin in USD 16 | */ 17 | price: Number, 18 | /** 19 | * Amount of bitcoin bought or sold 20 | * @readonly 21 | */ 22 | size: {type: Number, default: 0}, 23 | /** 24 | * Current status of the rule. Possible values: 25 | * idle - the Rule is turned off by user 26 | * bought - result of a BUY 27 | * sold - result of a SELL 28 | * pending - pending BUY or SELL transaction 29 | */ 30 | status: {type: String, default: 'idle'}, 31 | /** 32 | * Percentage that this rule represents of the entire account funds 33 | * @example 100% 34 | */ 35 | portfolioDiversity: {type: Number, default: 100}, 36 | /** 37 | * Highest value the rule had held since it was bought. This will drive the stop loss price 38 | */ 39 | high: {type: Number, default: 0}, 40 | /** 41 | * Price per bitcoin that, if reached, will trigger a limit SELL 42 | * Only triggers a SELL if status is 'bought' 43 | * @example 1% 44 | */ 45 | sellStrategyPerc: {type: Number, default: .1}, 46 | stopLossPrice: Number, 47 | /** 48 | * Lowest value the rule had held since it was sold. This will drive the limit sell price 49 | */ 50 | low: {type: Number, default: 0}, 51 | /** 52 | * Price per bitcoin that, if reached, will trigger a limit BUY 53 | * Only triggers a BUY if status is 'sold' 54 | * @example 1% 55 | */ 56 | limitPerc: {type: Number, default: .05}, 57 | limitPrice: Number, 58 | /** 59 | * Price per bitcoin that, if reached, will trigger a market SELL and will put the rule on 'idle' state 60 | * TODO: handle risk logic 61 | */ 62 | riskPerc: {type: Number, default: 10}, 63 | riskPrice: Number, 64 | /** 65 | * Order id of active BUY or SELL limit order 66 | */ 67 | limitOrderId: String, 68 | /** 69 | * Last RSI(14) value recorded 70 | */ 71 | lastRsi: Number 72 | }); 73 | 74 | /** 75 | * Calculates all the dynamic fields on the rule 76 | * @param rule 77 | */ 78 | const validateRule = (rule) => { 79 | // Upwards movement 80 | if (rule.status === 'bought' && (rule.high < rule.price || rule.high === 0)) { 81 | rule.high = rule.price; 82 | rule.riskPrice = rule.high - (rule.high * rule.riskPerc / 100); 83 | rule.stopLossPrice = rule.high - (rule.high * rule.sellStrategyPerc / 100); 84 | rule.riskPrice = Utils.precisionRound(rule.riskPrice, 2); 85 | rule.stopLossPrice = Utils.precisionRound(rule.stopLossPrice, 2); 86 | } 87 | 88 | // Downwards movement 89 | if (rule.status === 'sold' && (rule.low > rule.price || rule.low === 0)) { 90 | rule.low = rule.price; 91 | rule.limitPrice = rule.low + (rule.low * rule.limitPerc / 100); 92 | rule.limitPrice = Utils.precisionRound(rule.limitPrice, 2); 93 | } 94 | }; 95 | 96 | /** 97 | * Before persisting a rule, update docinfo 98 | */ 99 | Rule.pre('save', function preSave(next) { 100 | const rule = this; 101 | const now = new Date().toISOString(); 102 | 103 | // Update doc info 104 | rule.set('docinfo.updatedAt', now); 105 | if (!rule.get('docinfo.createdAt')) { 106 | rule.set('docinfo.createdAt', now); 107 | } 108 | 109 | // Calculate fields 110 | validateRule(rule); 111 | 112 | next(); 113 | }); 114 | 115 | module.exports = { 116 | Rule: mongoose.model('Rule', Rule), 117 | validateRule 118 | }; -------------------------------------------------------------------------------- /src/services/rbhApiService.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise-native'); 2 | const { RBH_API_BASE, NUMMUS_RH_API_BASE, RH_CREDENTIALS } = require('../../config/env'); 3 | 4 | const common = { json: true }; 5 | const TOKEN_REFRESH_INTERVAL = 18000000; // 5h 6 | 7 | class RHService { 8 | constructor() { 9 | this.commonPrivate = { ...common, headers: {} }; 10 | } 11 | 12 | auth() { 13 | const options = { 14 | ...common, 15 | method: 'POST', 16 | uri: `${RBH_API_BASE}/oauth2/token/`, 17 | form: { 18 | ...RH_CREDENTIALS, 19 | grant_type: 'password', 20 | scope: 'internal', 21 | expires_in: TOKEN_REFRESH_INTERVAL // 5h 22 | } 23 | }; 24 | return request(options) 25 | .then(({ access_token, token_type }) => this.commonPrivate.headers.Authorization = `${token_type} ${access_token}`); 26 | } 27 | 28 | /** 29 | * Even though it seems like RH supports multiple accounts for a user, for now we will not... 30 | */ 31 | getAccount() { 32 | return this.getWithAuth( `${RBH_API_BASE}/accounts/`) 33 | .then(({ results }) => results[0]); 34 | } 35 | 36 | /** 37 | * Even though it seems like RH supports multiple accounts for a user, for now we will not... 38 | */ 39 | getCryptoAccount() { 40 | return this.getWithAuth( `${NUMMUS_RH_API_BASE}/accounts/`) 41 | .then(({ results }) => results[0]); 42 | } 43 | 44 | /** 45 | * Only available for crypto 46 | * @returns {Promise.|Promise} 47 | */ 48 | getHoldings() { 49 | return this.getWithAuth( `${NUMMUS_RH_API_BASE}/holdings/`) 50 | .then(({ results = [] }) => results.filter(({ quantity }) => Number(quantity))); 51 | } 52 | 53 | /** 54 | * Get crypto orders 55 | * @returns {*} 56 | */ 57 | getOrders() { 58 | return this.getWithAuth(`${NUMMUS_RH_API_BASE}/orders/`) 59 | .then(({ results = [] }) => results); 60 | } 61 | 62 | placeOrder(order) { 63 | return this.postWithAuth(`${NUMMUS_RH_API_BASE}/orders/`, order); 64 | } 65 | 66 | /** 67 | * Gets the first currency which its asset currency is 'symbol' 68 | * @param symbol 69 | * @returns {Promise.|Promise} 70 | */ 71 | getCurrencyPairs(symbol) { 72 | return this.getWithAuth(`${NUMMUS_RH_API_BASE}/currency_pairs/?symbol=${symbol}`) 73 | .then(({ results = [] }) => 74 | symbol ? results.find(({ asset_currency }) => asset_currency.code === symbol) : results); 75 | } 76 | 77 | /** 78 | * Get historical values for passed currency pair id 79 | * @param pairId Currency pair id 80 | * @returns {Promise.|Promise} 81 | */ 82 | getHistoricals(pairId) { 83 | return this.getWithAuth(`${RBH_API_BASE}/marketdata/forex/historicals/${pairId}/?span=hour&interval=15second&bounds=24_7`) 84 | .then(({ data_points = [] }) => data_points); 85 | } 86 | 87 | /** 88 | * Get quote. Used for interval feed analysis 89 | * @param id 90 | * @returns {Promise|Promise.} 91 | */ 92 | getQuote(id) { 93 | return this.getWithAuth(`${RBH_API_BASE}/marketdata/forex/quotes/${id}/`); 94 | } 95 | 96 | /** 97 | * Generic GET request with authentication headers 98 | * @param uri 99 | * @returns {*} 100 | */ 101 | getWithAuth(uri) { 102 | const options = { 103 | ...this.commonPrivate, 104 | uri, 105 | }; 106 | return request(options); 107 | } 108 | 109 | /** 110 | * Generic POST request with authentication headers 111 | * @param uri 112 | * @param body 113 | * @param customOption 114 | * @returns {*} 115 | */ 116 | postWithAuth(uri, body, customOption = {}) { 117 | const options = { 118 | ...this.commonPrivate, 119 | method: 'POST', 120 | uri, 121 | body, 122 | ...customOption, 123 | }; 124 | return request(options); 125 | } 126 | } 127 | 128 | module.exports = new RHService(); -------------------------------------------------------------------------------- /src/engine/Engine.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const uuid = require('uuid/v1'); 3 | const rh = require('../services/rbhApiService'); 4 | const mailer = require('../services/mailService'); 5 | const Utils = require('../utils'); 6 | 7 | const TOKEN_REFRESH_INTERVAL = 18000000; // 5h 8 | const REFRESH_INTERVAL = 10000; // 1s 9 | 10 | // Saved in memory for now, will be fetched from DB soon 11 | const rule = { 12 | currency_code: 'BTC', 13 | portfolioDiversity: 1, 14 | sellStrategyPerc: 1, 15 | }; 16 | 17 | class Engine { 18 | constructor() { 19 | this.currencyPair = null; 20 | this.limitBuyPrice = null; 21 | this.limitSellPrice = null; 22 | } 23 | 24 | async start() { 25 | try { 26 | await rh.auth(); 27 | this.currencyPair = await rh.getCurrencyPairs(rule.currency_code); 28 | this.processFeeds(); 29 | 30 | // Refresh token and process feeds every 5 hours and 10 secs respectively 31 | setInterval(() => rh.auth(), TOKEN_REFRESH_INTERVAL); 32 | setInterval(async () => this.processFeeds(), REFRESH_INTERVAL); 33 | } catch (error) { 34 | console.error(error); 35 | } 36 | } 37 | 38 | async processFeeds() { 39 | try { 40 | const [account, cryptoAccount, holdings, updatedCurrencyPair, orders, historicals, quote] = await Promise.all([ 41 | rh.getAccount(), 42 | rh.getCryptoAccount(), 43 | rh.getHoldings(), 44 | rh.getCurrencyPairs(rule.currency_code), 45 | rh.getOrders(), 46 | rh.getHistoricals(this.currencyPair.id), 47 | rh.getQuote(this.currencyPair.id) 48 | ]); 49 | this.currencyPair = updatedCurrencyPair; 50 | const holding = holdings.find(({ currency }) => currency.code === rule.currency_code); 51 | const usdBalanceAvailable = Number(account.sma); 52 | const investedCurrencyBalance = Number(_.get(holding, 'quantity', 0)); 53 | const currentPrice = Number(quote.mark_price || 0); 54 | const lastOrder = orders.length && orders[0]; 55 | const account_id = _.get(lastOrder || holding, 'account_id'); 56 | const RSI = Utils.calculateRSI(historicals); 57 | 58 | // Purchase Pattern 59 | if (usdBalanceAvailable && !investedCurrencyBalance) { 60 | // Price not longer oversold 61 | if (RSI > 30) { 62 | this.limitBuyPrice = null; 63 | // Cancel order and exit 64 | return await this.cancelLastOrder(lastOrder); 65 | } 66 | // If limit not set, set it and exit until next tick 67 | if (!this.limitBuyPrice) { 68 | this.limitBuyPrice = currentPrice; 69 | return; 70 | } 71 | // Price went down and RSI is below 30 72 | if (this.limitBuyPrice > currentPrice) { 73 | // Update limit 74 | this.limitBuyPrice = currentPrice; 75 | // Cancel last order, exit and wait 76 | return await this.cancelLastOrder(lastOrder); 77 | } 78 | // If current price went above the limit price, this means the ticker 79 | // could be trying to go out of oversold, therefore buy here. 80 | if (this.limitBuyPrice < currentPrice) { 81 | // Cancel possible pending order 82 | await this.cancelLastOrder(lastOrder); 83 | // Buy 0.02% higher price than market price to get an easier fill 84 | const price = (currentPrice * 1.0002).toFixed(2).toString(); 85 | const quantity = Utils.calculateCurrencyAmount(price, account.sma, rule.portfolioDiversity); 86 | return await this.placeOrder(account_id, quantity, price, this.currencyPair.id, 'buy'); 87 | } 88 | } 89 | // Sell pattern 90 | else if (investedCurrencyBalance) { 91 | const purchasePrice = Number(lastOrder.quantity); 92 | const overbought = RSI >= 70; 93 | // If limit not set, put a stop loss at -.5% of the original purchase price 94 | if (!this.limitSellPrice) { 95 | this.limitSellPrice = this.getLimitSellPrice(purchasePrice, { initial: true }); 96 | return; 97 | } 98 | // Cancel a possible pending order 99 | await this.cancelLastOrder(lastOrder); 100 | // If stop loss hit, sell immediate 101 | if (currentPrice <= this.limitSellPrice) { 102 | // Sell 0.02% lower price than market price to get an easier fill 103 | const price = (currentPrice * 0.9998).toFixed(2).toString(); 104 | return await this.placeOrder(account_id, investedCurrencyBalance, price, this.currencyPair.id, 'sell'); 105 | } 106 | // Increase limit sell price as the current price increases, do not move it if price decreases 107 | const newLimit = this.getLimitSellPrice(currentPrice, { overbought }); 108 | if (newLimit > this.limitSellPrice) { 109 | this.limitSellPrice = newLimit; 110 | } 111 | } 112 | } catch (error) { 113 | console.debug({ error }, 'Error occurred during processFeeds execution'); 114 | } 115 | } 116 | 117 | /** 118 | * Helper function to cancel last order if it exists 119 | * @param order 120 | * @returns {Promise.<*>} 121 | */ 122 | cancelLastOrder(order) { 123 | if (_.get(order, 'cancel_url')) { 124 | console.debug(Utils.formatJSON(order, 0), 'Canceling order'); 125 | mailer.send({ text: `Canceling Order: ${Utils.formatJSON(order)}`}) 126 | return rh.postWithAuth(order.cancel_url); 127 | } 128 | return Promise.resolve(); 129 | } 130 | 131 | /** 132 | * Helper function to place an order 133 | * @param account_id 134 | * @param quantity 135 | * @param price 136 | * @param currency_pair_id 137 | * @param side 138 | * @returns {*} 139 | */ 140 | placeOrder(account_id, quantity, price, currency_pair_id, side) { 141 | const order = { 142 | account_id, 143 | quantity, 144 | price, 145 | currency_pair_id, 146 | side, 147 | time_in_force: 'gtc', 148 | type: 'limit', 149 | ref_id: uuid() 150 | }; 151 | console.debug(Utils.formatJSON(order, 0), 'Placing order'); 152 | mailer.send({ text: `Placed Order: ${Utils.formatJSON(order)}`}); 153 | return rh.placeOrder(order); 154 | } 155 | 156 | /** 157 | * Calculates stop loss price based on rule config. 158 | * Note: On initialization and oversold indicator the stop loss percentage from the rule is 159 | * divided by two in order to minimize risk and maximize profits respectively 160 | * @param price 161 | * @param options 162 | * @returns {number} 163 | */ 164 | getLimitSellPrice(price, options = {}) { 165 | const { initial, overbought } = options; 166 | const percentage = (initial || overbought) ? rule.sellStrategyPerc / 2 : rule.sellStrategyPerc; 167 | return price - (price * (percentage / 100)); 168 | } 169 | } 170 | 171 | module.exports = Engine; --------------------------------------------------------------------------------