crypto-nodes
Version:
512 lines (349 loc) • 10.8 kB
JavaScript
var redis = require("redis");
var async = require('async');
var orderCache = {
prefix: 'crypto-order-',
cache: {},
errors: {},
error_limit: 3,
error_tick: 1000,
register_error(exchange, error) {
if(!this.errors[exchange]) {
this.errors[exchange] = [];
}
this.errors[exchange].push(error);
if(this.errors[exchange].length > error_limit) {
// Need to disable exchange
orderManagerEmit(this.node, 'orderbook_op', { payload: { op: "disable_exchange", exchange: exchange }});
}
},
init: function (node) {
client = redis.createClient('6379', process.env.REDIS_SERVER || 'node1.builder-apps.org', { password: process.env.REDIS_PASSWORD });
var that = this;
this.node = node;
if(this.error_timer) {
clearInterval(this.error_timer);
}
// Remove 1 error every tick from each exchange
this.error_timer = setInterval(function () {
for(x in that.errors) {
if(that.errors[x].length) {
that.errors[x].shift();
}
}
}, this.error_tick);
// Read all orders from redis
function callback(orders) {
console.log(orders);
}
client.keys('*', function (err, keys) {
if (err) return console.log(err);
if(keys) {
var snapshot_keys = [];
for(x in keys) {
if(keys[x].substr(0, that.prefix.length) == that.prefix) {
snapshot_keys.push(keys[x]);
} else {
console.log(keys[x]);
}
}
async.map(snapshot_keys, function(key, cb) {
client.get(key, function (error, value) {
if (error) {
return cb(null, false);
} else {
return cb(null, value);
}
});
}, function (error, results) {
var out = [];
if(!error && results.length) {
for(x in results) {
try {
var order = JSON.parse(results[x]);
out.push(order);
} catch(e) {
}
}
}
callback(out);
});
}
});
},
process: function(order) {
// 2. Save incoming orders in simple format under <XXX ID>
// This should trigger in order ADD / ACCEPT and should modify redis data as being the most up to date
var order_id = order.account_;
client.set(this.prefix + order_id, JSON.stringify(order));
},
update: function(order_id, filled) {
// 3. If we have order filled events, substract from redis crypto-order-<XXX ID> the filled QTY
client.get(this.prefix + order_id, function(err, reply) {
if(reply) {
try {
var order = JSON.parse(reply);
order.order_qty_ = order.order_qty_ - filled;
client.set(this.prefix + order_id, JSON.stringify(order));
} catch (e) {}
}
});
},
get: function(order_id, callback) {
// 4. Read the current order state from redis, we need to add it again to the orderbook because:
// A. It was partially processed and IOC triggered by exchanged
// B. Exchange returned error either BALANCE or NETWORK or DDOS
client.get(this.prefix + order_id, function(err, reply) {
var order = false;
if(reply) {
try {
order = JSON.parse(reply);
} catch (e) {}
}
callback(order);
});
}
}
module.exports = function(RED) {
function orderManager(config) {
RED.nodes.createNode(this,config);
var node = this;
orderCache.init(node);
var processed = {
add: 0,
accept: 0,
cancel: 0,
replace: 0,
fill: 0,
error: 0
}
console.log('Order Manager Online!');
console.log('Debug Data');
setTimeout(function () {
orderManagerEmit(node, 'exchange_op', { payload: { exchange: 'all', op: 'get_balance' } });
}, 1000);
setInterval(function () {
node.status({fill:"green",shape:"dot", text: JSON.stringify(processed)});
}, 1000);
node.on('close', function() {
om_http.close();
});
node.on('input', function(msg) {
// console.log(msg);
// Internal functions
if(msg && msg.payload && msg.payload.op) {
if(msg.payload.op == 'order_log') {
if(msg.payload.data && msg.payload.data.exchange) {
console.log("Updating Balance");
orderManagerEmit(node, 'exchange_op', { payload: { exchange: msg.payload.data.exchange, op: 'get_balance' } });
}
switch(msg.payload.status) {
case 'ADDED':
break;
case 'DONE':
// Order Completed
break;
case 'CANCEL':
// Order was cancelled before fully filled
// Return Order to orderbook
break;
case 'ERROR':
// Order was cancelled before fully filled
if(msg.payload.msg == 'BALANCE') {
// Should be handled elsewhere
} else {
// Generic exchange error
// Increment exchange error counter
// If 3 errors in 10 seconds
// Disable exchange for 1 minute (or similar)
}
break;
}
if(msg.payload.status == 'CANCEL' || msg.payload.status == 'ERROR') {
// These 2 statuses require returning the order to orderbook
}
console.log('ORDERLOG ::')
console.log(msg.payload);
orderManagerEmit(node, 'order_status', { payload: msg.payload });
}
if(msg.payload.op == 'exchange_order') {
console.log(msg);
}
// We need to forward balances data to orderbook
if(msg.payload.op == 'get_balance') {
msg.payload.type = 'balances';
orderManagerEmit(node, 'orderbook_op', msg);
}
} else {
// Check for orderbook event
if(msg.key) {
if(processed.hasOwnProperty(msg.key)) {
processed[msg.key]++;
}
var tx = orderManagerProcessTX(msg);
// console.log(tx);
if(tx.op) {
console.log('OP ::');
console.log(tx);
// If its a fill on exchange, forward it to exchanges
if(tx.op == 'exchange_order' && tx.exchange) {
orderManagerEmit(node, 'exchange_op', { payload: { exchange: tx.exchange, op: tx.op, tx: tx } });
}
// Inhouse order emits multiple events
if(tx.op == 'inhouse_order') {
for(x in tx.emits) {
orderManagerEmit(node, 'order_status', { payload: tx.emits[x] });
}
} else {
// Emit single event
orderManagerEmit(node, 'order_status', { payload: tx });
}
}
if(tx.status) {
//console.log(tx);
if(tx.data && tx.data.data && tx.data.data.order) {
var order = tx.data.data.order;
// We have original order information here
console.log('STATUS ::');
console.log(order);
// Lets store it in redis in case we need to return it to orderbook
orderCache.process(order);
orderManagerEmit(node, 'order_status', { payload: tx });
}
}
} else {
// Not implemented?
if(msg.payload.status == 'disconnected') {
return;
}
if(msg.payload.status == 'connected') {
return;
}
console.log('orderManager missing EVENT');
console.log(msg);
}
//console.log('OB EVENT::');
// Orderbook events like fill..
//console.log(msg);
}
});
}
function orderManagerProcessTX(msg) {
var tx = {};
console.log(msg);
switch(msg.key) {
case 'add':
case 'added':
tx.status = 'ADDED';
tx.order_id = msg.data.account_id;
tx.data = msg;
break;
case 'accept':
tx.status = 'ACCEPT';
tx.order_id = msg.data.order.account_;
tx.data = msg;
// Order added
break;
case 'fill':
var o_inbound = msg.data.inbound_order;
var o_matched = msg.data.matched_order;
var i_house = o_inbound.exchange == 'internal';
var m_house = o_matched.exchange == 'internal';
tx = {
op: 'exchange_order',
symbol: msg.data.symbol,
qty: msg.data.fill_qty,
price: msg.data.fill_price,
emits: [],
}
if(i_house && m_house) {
tx.op = 'inhouse_order';
tx.emits.push({
status:'FILLED',
order_id: o_inbound.account_,
filled_amount: msg.data.fill_qty,
data: msg.data
});
tx.emits.push({
status:'FILLED',
order_id: o_matched.account_,
filled_amount: msg.data.fill_qty,
data: msg.data
});
if(o_inbound.open_qty_ == 0) {
tx.emits.push({
status:'DONE',
order_id: o_inbound.account_,
data: msg.data
});
}
if(o_matched.open_qty_ == 0) {
tx.emits.push({
status:'DONE',
order_id: o_matched.account_,
data: msg.data
});
}
// TRANSFER $$ BETWEEN ACCOUNTS
} else if(!i_house && !m_house) {
// TRANSFER BETWEEN 2 EXCHANGES, WE DONT SUPPORT THIS YET..
// OR .. TODO ..
} else {
// Inbound order is internal
if(i_house) {
tx.exchange = o_matched.exchange;
tx.is_buy = !o_matched.is_buy_;
tx.cost = o_matched.cost;
tx.o_qty = o_matched.order_qty_;
tx.acc = o_inbound.account_;
tx.o_price = o_inbound.price_;
tx.o_cost = o_inbound.cost;
}
// Matched order is internal
if(m_house) {
tx.exchange = o_inbound.exchange;
tx.is_buy = !o_inbound.is_buy_;
tx.cost = o_inbound.cost;
tx.o_qty = o_inbound.order_qty_;
tx.acc = o_matched.account_;
tx.o_price = o_matched.price_;
tx.o_cost = o_matched.cost;
}
}
break;
}
//console.log(tx);
return tx;
}
function orderManagerAddOrder(node, order) {
/*
var payload = {
type: 'add',
from: order.symbol.split('/')[0],
to: order.symbol.split('/')[1],
is_buy:
priority: 10,
}
var order = { s: msg.payload.from + '/' + msg.payload.to , b: (msg.payload.is_buy ? true : false), p: msg.payload.price, q: msg.payload.quantity, a: msg.payload.account_id };
orderManagerEmit(node, 'orderbook_op', { payload: payload });
*/
}
function orderManagerEmit(node, type, msg) {
var outputs = [ 'exchange_op', 'orderbook_op', 'order_status', 'error' ];
var out = [];
for(x in outputs) {
if(type == outputs[x]) {
out.push(msg);
} else {
out.push(null);
}
}
if(type == 'order_status') {
// Check for errors, disable exchanges..
}
//console.log('EMITTING ::');
//console.log(out);
node.send(out);
}
RED.nodes.registerType("orderManager",orderManager);
RED.nodes.registerType("redis_server",function () {});
}