I would build something like this for the right amount of money, but i'm not just going to build it and deliver it without being paid hourly to develop it. I started tinkering with the idea about 6 months ago, but because of work i haven't really messed with it in a while. Here's a model trade engine i was developing in nodejs. the idea is that you have a market object of shape {coin_a,coin_b,fee}, and each instance of `TradeEngine` is an in memory orderbook for trading, to be used with a database connnector of some type for persistence. this is only a demo, it should never be used in production and if you do i wouldn't be surprised if it has rounding errors and race conditions, its just a model, for reference only. Were i to be hired to do such a job, i would go for a safer design that utilized atomic operations and converting the satoshi amount to integers (or with a library for storing them in some representation container) with a database to prevent race conditions, undefined behavior, and rounding errors.
// const MKT = {
// coin_a: 'btc',
// coin_b: 'eth',
// fee: .005
// };
import Promise from 'bluebird';
const MAX_DEPTH = 100;//maximum recursion depth for trade algorithm
const STATE_RUN = 1;
const STATE_HALT = 0;
const STATE_WILL_EXIT = 9;
const STATE_DB_ERR = 2;
const DataAdapterInterface = {
get_orderbook: { type: 'function', returnValue: 'Promise' },
balance_incr: { type: 'function', returnValue: 'Promise' },
balance_decr: { type: 'function', returnValue: 'Promise' },
add_trade: { type: 'function', returnValue: 'Promise' },
cancel_trade: { type: 'function', returnValue: 'Promise' },
onerror: { type: 'function', returnValue: 'undefined' }
};
//uuid generator
uuid = a=>a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b);
class TradeEngine {
constructor( mkt, DatabaseAdapter ){
const MKT = mkt;
this.db = DatabaseAdapter;
this.i;
this.engine = STATE_RUNNING;
this.balances = {};
this.trades = {
buy: [],
sell: []
};
this.queue = [];
}
//generates a new trade id.
generate_id(){
return `${MKT.coin_a} - ${MKT.coin_b} ${uuid()}`;
}
// adds a trade to the order books.
add_trade( user, type, amount, price ){
let total = amount * price;
if(type == 'buy'){
total += (total * FEE);
}
let the_coin = type == 'buy' ? MKT.coin_a : MKT.coin_b;
let the_amount = type == 'buy' ? total : amount;
if(this.balances[user][ the_coin ] < the_amount){
return false;//insufficient funds.
}
this.balances[user][the_coin] -= the_amount;
let id = this.generate_id();
//arrays always maintain sort order
// Descending for Buys
// Ascending for Sells
// 1. find the insert position for the new item
let i = 0;
switch(type){
case 'sell':
// sell array is ordered lowest to highest
// we want to iterate until we find the first price that is greater than our price
// we will use splice to insert at that position, pushing existing elements back
while( i break;
case 'buy':
// buy array is ordered highest to lowest
// we want to iterate until we find the first price that is less than our price
while( i < this.trades[type].length && this.trades[type][i].price >= price){ i++; }
break;
}
// 2. insert the item at the determined position
this.trades[type].splice(i,0,{
id: id,
user: user,
total: total,
amount: amount,
price: price
});
return id;
}
//cancel trade
cancel_trade( type, id, callback ){
//find the trade and remove it.
let i = 0;
while(i < this.trades[type].length && this.trades[type][i].id !== id){ i++; }
let row = trades[type][i];
trades[type].splice(i,1);//remove trade.
//credit the balance back to the user it belongs to.
let the_coin = type == 'buy' ? MKT.coin_a : MKT.coin_b;
let the_amount = type == 'buy' ? total : amount;
this.balances[row.user][ the_coin ] += the_amount;
}
//process the order book
process_trades(){
return new Promise((req,res)=>{
let buy = this.trades.buy[0];
let sell = this.trades.sell[0];
if( buy.price >= sell.price ){
let price = (buy.price - sell.price > 0) ? buy.price : sell.price;
let amount = buy.amount;
if(buy.amount > sell.amount){
amount = sell.amount;
}
let total = price * amount;
let sell_fee = total * .005;
let buy_fee = amount * .005;
sell.amount -= amount;
buy.amount -= amount;
sell.total -= total;
buy.total -= (total + sell_fee);
this.balances.system[MKT.coin_a] += sell_fee;
this.balances.system[MKT.coin_b] += buy_fee;
this.balances[sell.user][MKT.coin_a] += total;
this.balances[buy.user][MKT.coin_b] += (amount - buy_fee);
if(sell.amount == 0){
this.trades.sell.shift();
}
if(buy.amount == 0){
this.trades.buy.shift();
}
i++;
if(i < MAX_DEPTH){
return this.process_trades(); // recurse
}
}
return resolve();//recursion ends.
});
}
//processes the action queue
process_queue(){
const q = this.queue.slice();
this.queue.length = 0;
for(let i=0;i switch(q[i].action){
case 'deposit':
this.balances[q[i].user][q[i].coin] += q[i].amount;
// @todo ack_deposit
break;
case 'withdrawal':
this.balances[q[i].user][q[i].coin] -= q[i].amount;
// @todo validate then ack_withdrawal
break;
case 'add_trade':
this.add_trade( q[i].user, q[i].type, q[i].amount, q[i].price );
break;
case 'cancel_trade':
this.cancel_trade( q[i].type, q[i].id );
break;
case 'set_engine':
if([STATE_RUN,STATE_HALT].indexOf(q[i].engine) !== -1){
this.engine = q[i].engine;
}
break;
}
}
}
loop(){
if(this.engine === STATE_WILL_EXIT){
return;
}
if(this.engine === STATE_HALT){
setTimeout(this.loop,5000);
}
if(this.engine === STATE_RUN){
i = 0;//count recursions so we don't overflow the stack
this.process_trades().then(()=>setImmediate(()=>this.loop()));
}
}
start(){
this.loop();
}
}
export default TradeEngine;