Skip to content

Instantly share code, notes, and snippets.

@snowkidind
Created June 17, 2018 03:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save snowkidind/47fa07c7a2145161554c7997ff625ecd to your computer and use it in GitHub Desktop.
Save snowkidind/47fa07c7a2145161554c7997ff625ecd to your computer and use it in GitHub Desktop.
Binance structural idea for trading bot asset management, in progress
let db = require('./dbaccess');
let utilities = require('./utilities');
let binance = require('./binance');
let chalk = require('chalk');
// manage tokens
// manage balances
// delegate profits, fees and taxes
// the token cycle is the process of buying, selling and delegating the costs associated
// with the trade in order to 1. identify what state the application is in, or 2. to have
// separate control of token balances so that things can be systematically be set aside
// for operating costs such as fees, taxes and profits
let dOptions;
let tokenObj = {};
let balancesObject = {};
let transactionId;
let closeTxData;
let initTxData;
module.exports = {
initToken:function(daemonOptions, ready){
dOptions = daemonOptions;
// load token from database or if doesn't exist, initialize token object
db.getToken(function(token){
if (0){ // some of this code is still experimental. after all its a gist...
// console.log("token exists in db");
tokenObj = token;
// synchronize balances with asset manager
ready(true);
}
else {
// console.log("generating new token object");
generateToken(function(tokenObj){
db.saveToken(tokenObj);
ready(true);
});
}
});
},
// update token object balances
updateCycleState:function(stream){
// poll tether and usdt for balances
const tetherIndex = stream.B.map(function (x) {
return x.a;
}).indexOf('USDT');
const btcIndex = stream.B.map(function (x) {
return x.a;
}).indexOf('BTC');
let tetherBalance = stream.B[tetherIndex].f;
let btcBalance = stream.B[btcIndex].f;
const serverBalances = { tether:tetherBalance, btc: btcBalance };
recalculateBalances(serverBalances, null, function(){
updateTokenBalances(stream);
});
},
// initialize a transaction with the parent amount and the fee...
initializeTransaction:function(initializeTransactionData){
initTxData = initializeTransactionData; // store the payload
// just init a bare bones tx bc it will get filled in by the actual cycle state update
db.initializeTransaction(initializeTransactionData.pair, function(txId){
// gather an id in order to access this tx later.
transactionId = txId;
});
},
// should never be called before a transaction is initialized
openTransaction:function(buyData){
// object Model
// let buy = {
// "symbol":"BTCUSDT",
// "orderId":119571607,
// "clientOrderId":
// "7rUtKmq4Ceh6HbgXuVuN9O",
// "transactTime":1529154731529,
// "price":"0.00000000",
// "origQty":"0.00356000",
// "executedQty":"0.00356000",
// "status":"FILLED",
// "timeInForce":"GTC",
// "type":"MARKET",
// "side":"BUY"
// };
// looking for a transaction object. transactionId should be set at this point
findTransaction(function(transaction) {
// stage two is to open the transaction object on balance update
if (dOptions.tradeToAltcoins){
// TODO code in openTransaction for altcoins...
console.log("you need to code in openTransaction for altcoins")
}
else {
db.openTransaction(transaction.id, buyData.executedQty, initTxData.parentAmt, initTxData.last, initTxData.feeBuy, function(results){
transactionId = transaction.id;
});
}
});
},
closeTransaction:function(sellData){
// let sell = {
// "symbol":"BTCUSDT",
// "orderId":119572142,
// "clientOrderId":"OMYpzj08N68Es2HynYVF1D",
// "transactTime":1529154759758,
// "price":"0.00000000",
// "origQty":"0.00356000",
// "executedQty":"0.00356000",
// "status":"FILLED",
// "timeInForce":"GTC",
// "type":"MARKET",
// "side":"SELL"
// };
closeTxData = { pair: sellData.symbol,qty:sellData.executedQty };
findTransaction(function(transaction) {
transaction.isClosed = 1;
// first close the transaction object
closeTransactionObject(transaction, function (closedTransaction) {
generateToken(function(tokenObj){
db.saveToken(tokenObj);
});
});
});
// Wondering if this will come about...
if (sellData.status !== "FILLED") {
console.log(chalk.bgMagenta(" Status was not filled "));
console.log(chalk.magenta(JSON.stringify(sellData)));
}
},
hasOpenTransactions: function(){
findTransaction(function(transaction){
if (transaction){
if (transaction.isClosed === 0){
return true;
}
}
else {
return false;
}
});
},
balances:function(){
return balancesObject;
},
token:function(){
// returns the token object for other modules to call upon
return tokenObj;
},
};
function findTransaction(callback){
// if have the transactionid query database by id else poll db for open tx object
if (transactionId > 0) {
db.getTransaction(transactionId, function(transactionObject){
callback(transactionObject);
});
}
// else determine if there is no open tx object
else {
db.getOpenTransactionObject(function(transactionObject){
if (transactionObject){
callback(transactionObject);
}
else {
// if fails, unable to find an open transaction, return a null transaction to indicate outness.
console.log("Unable to find a transaction");
callback(null);
}
});
}
}
function closeTransactionObject(transaction, callback){
let isClosed = 1;
// will cll this again in just a minute
db.closeTransaction(transaction.id, isClosed, closeTxData.qty, 0, 0, 0, 0, 0, 0, 0, 0, 0, function(result){
if (result){
// concern here is that binance only sold a portion of the total
// amount and multiple cycleStates will be called until order is fulfilled.. Not sure...
// objective here is to add the rest of the fields to the transaction object, update the database.
callback(transaction); // closed transaction
}
else {
callback(transaction); // send it anyway
console.log("there was an error at db.closeTransactionObject");
}
});
}
function updateTokenBalances(stream){
const tetherIndex = stream.B.map(function (x) {
return x.a;
}).indexOf('USDT');
tokenObj.onOrder = stream.B[tetherIndex].l;
tokenObj.balance = balancesObject.usdt.availableBalance;
const btcIndex = stream.B.map(function (x) {
return x.a;
}).indexOf('BTC');
tokenObj.tokens[0].onOrder = stream.B[btcIndex].l;
// console.log("before...");
// console.log(balancesObject);
// console.log("setting btc balance (stream) to: " + balancesObject.btc.availableBalance);
tokenObj.tokens[0].balance = balancesObject.btc.availableBalance;
// iterate tether tradeable tokens and load into token database
for (let i = 0; i < dOptions.tokens.tokens.length; i++) {
// altcoin balance will always be altcoin balance
const altIndex = stream.B.map(function (x) {
return x.a;
}).indexOf(dOptions.altToken);
// need to iterate alts now
for (let j = 0; j < tokenObj.tokens[i].tokens.length; j++) {
// Altcoin balances are allowed to pass through directly
tokenObj.tokens[i].tokens[j].balance = stream.B[altIndex].f;
tokenObj.tokens[i].tokens[j].onOrder = stream.B[altIndex].l;
}
}
}
// take raw server balances and account for holds and apply to balances object
function recalculateBalances(serverBalances, transaction, callback){
console.log(chalk.yellow("recalculateBalances", JSON.stringify(serverBalances), JSON.stringify(transaction) ));
// prototype the object,trash everything in the balances object in memory
balancesObject = {
usdt:{
profitsTaken:0,
taxesTaken:0,
feesTaken:0,
totalReserved:0,
availableBalance:0
} ,
btc:{
profitsTaken:0,
taxesTaken:0,
feesTaken:0,
totalReserved:0,
availableBalance:0
}
};
db.getBalanceObject(function(oldBalances){
// the balances object is stored in the database and contains
// the retained balances for the application
// the payout is as follows:
// fee is 1% (two half percent transactions)
// tax is 30% after fee
// profit deduction is 29%
// the rest of gains 40% are reinvested
if (oldBalances){
// determine tether balance
let totalReservedUSDT = Number(oldBalances.usdt.profitsTaken) + Number(oldBalances.usdt.taxesTaken) + Number(oldBalances.usdt.feesTaken);
let availableBalanceUSDT = Number(serverBalances.tether) - Number(totalReservedUSDT);
balancesObject.usdt.profitsTaken = oldBalances.usdt.profitsTaken;
balancesObject.usdt.taxesTaken = oldBalances.usdt.taxesTaken;
balancesObject.usdt.feesTaken = oldBalances.usdt.feesTaken;
balancesObject.usdt.totalReserved = totalReservedUSDT;
balancesObject.usdt.availableBalance = availableBalanceUSDT;
// determine btc balance
balancesObject.btc.totalReserved = Number(oldBalances.btc.profitsTaken) + Number(oldBalances.btc.taxesTaken) + Number(oldBalances.btc.feesTaken);
balancesObject.btc.availableBalance = Number(serverBalances.btc) - Number(balancesObject.btc.totalReserved);
balancesObject.btc.profitsTaken = oldBalances.btc.profitsTaken;
balancesObject.btc.taxesTaken = oldBalances.btc.taxesTaken;
balancesObject.btc.feesTaken = oldBalances.btc.feesTaken;
// if transaction object is null, it means that there are no open transactions, which implicates all in tether or btc
if (transaction !== null){
// meaning we are in to an alt / btc
if (transaction.isClosed === 0){
// console.log("transaction is not closed");
}
// meaning we are back out to btc / usdt
else if (transaction.isClosed === 1){
console.log("Pair", transaction.pair);
// find out how much in usd the token happens to be worth
binance.latestPrice(transaction.pair, function(last){
let valueParentOut = last * closeTxData.qty;
// console.log("calculating closed transaction...");
// console.log("Old in the new");
// console.log(serverBalances.tether, balancesObject.usdt.totalReserved)
// console.log("tether new: " + (serverBalances.tether - balancesObject.usdt.totalReserved)); // nan
// console.log("tether old: " + oldBalances.usdt.availableBalance); // ok
// console.log("btc new: " + (serverBalances.btc - balancesObject.btc.totalReserved)); // nan
// console.log("btc old: " + oldBalances.btc.availableBalance);
// let valueParentOut = serverBalances.tether - balancesObject.usdt.totalReserved - oldBalances.usdt.availableBalance;
console.log("valueParentOut", valueParentOut);
initTxData = {};
// if the tx is closed, balance will be either in btc or usdt (not in alts)
// need to get the difference - in bitcoin of what the transaction yielded
// calculate differences in the balances
const fees = valueParentOut * 0.0005; // 5 100ths of a percent for now
const totalGain = valueParentOut - transaction.valueParentIn - fees - transaction.feeBuy;
const taxWitheld = totalGain * 0.25; // 25 percent alloc for tax on gains
const profitTook = totalGain * 0.30;
const witholding = fees + transaction.feeBuy + taxWitheld + profitTook ;
// tax form 8949 fields
const netProceeds = valueParentOut - fees; //out price - fee2
const costBasis = transaction.valueParentIn + transaction.feeBuy; // inPrice + fee1
const gain = netProceeds - costBasis;
// new witholds only get added to current parent token
if (dOptions.tradeToAltcoins){
// all of this should be converted to dollars at the opportune moment
if (Math.sign(totalGain) === 1){
balancesObject.btc.profitsTaken += profitTook;
}
balancesObject.btc.taxesTaken += taxWitheld;
balancesObject.btc.feesTaken += fees + transaction.feeBuy;
balancesObject.btc.totalReserved += witholding;
balancesObject.btc.availableBalance = serverBalances.btc - balancesObject.btc.totalReserved;
balancesObject.usdt.availableBalance = serverBalances.usdt - balancesObject.usdt.totalReserved;
}
else {
// only withold profit if winning transaction
if (Math.sign(totalGain) === 1){
balancesObject.usdt.profitsTaken += profitTook;
}
// fees are always taken
balancesObject.usdt.feesTaken += fees + transaction.feeBuy;
// redeem tax losses if negative
balancesObject.usdt.taxesTaken += taxWitheld;
balancesObject.usdt.totalReserved += witholding;
balancesObject.btc.availableBalance = serverBalances.btc - balancesObject.btc.totalReserved;
balancesObject.usdt.availableBalance = serverBalances.tether - balancesObject.usdt.totalReserved;
}
console.log("Synchronize: ", transaction.id, 1, closeTxData.qty, valueParentOut, last, fees, totalGain, profitTook, taxWitheld);
db.saveBalanceObject(balancesObject);
// now synchronize the calculations with the db
db.closeTransaction(transaction.id, 1, closeTxData.qty, valueParentOut, last, fees, totalGain, profitTook, taxWitheld, netProceeds, costBasis, gain, function(){});
});
}
}
else {
// there is no transaction object. therefore what is, is what is
// so if you bought something with the bitcoin it would not really know about it
// if the app bought something and you sold it manually, the app would detect the open tx obj
// lets just get it working and consider contingencies later...
}
}
else {
// need to generate new balances object because FNF in database...
// basically put a null balances object in...
console.log("Warning - balancesObject Not Found. Creating new zeroed balances object...");
// save new balances object to database
db.saveBalanceObject(balancesObject);
}
callback();
});
// TODO: automate moving witholds to appropriate positions
// if balance on exchange left over is movable to a better hiding spot, execute:
// fee coverage to bnb
// profits to alternate stable coin or long position (ADA)
}
// scans a freshly updated token object and balances object to determine
// if there is a transaction that is unaccounted for
function reconcileUnknownTransaction(){
// console.log("Reconciling unknown transaction");
// console.log("tradeAlts: ", dOptions.tradeToAltcoins);
// console.log(JSON.stringify(tokenObj));
if (dOptions.tradeToAltcoins){
// TODO code in reconcileUnknownTransaction for altcoins...
console.log("you need to code in reconcileUnknownTransaction for altcoins")
}
else {
// trading in btcusdt
//console.log("btc balance", tokenObj.tokens[0].balance);
//console.log("btc orders", tokenObj.tokens[0].onOrder);
//console.log("btc balance", tokenObj.tokens[0].minimumQuantity);
let totalBtc = Number(tokenObj.tokens[0].balance) + Number(tokenObj.tokens[0].onOrder);
let releasedBtc = totalBtc - Number(balancesObject.btc.totalReserved);
if (releasedBtc > tokenObj.tokens[0].minimumQuantity){
// generate a new transaction object
db.initializeTransaction('BTCUSD', function(txId){
// TODO valueParentIn, feeBuy
db.openTransaction(txId, releasedBtc, 0, 0, function(results){
transactionId = txId;
console.log("added transaction buy " + releasedBtc + " for ??? usdt");
});
});
}
else {
console.log("btc balance does not suggest you are holding btc");
}
}
}
// needs to happen after the balances object is verified with the asset manager
function generateToken(cb){
console.log("generate token");
let theToken = dOptions.tokens;
// this is where the asset manager will assume control of the tokens.
binance.allBalances(function (balances) {
let tether = dOptions.tokens.token;
let tetherBalance = balances[tether].available;
let btcBalance = balances['BTC'].available;
const serverBalances = { tether:tetherBalance, btc: btcBalance };
findTransaction(function(transaction){
console.log("ServerBals in generateToken: ", serverBalances);
// console.log("Transaction: ", transaction);
// console.log("serverBalances: " + JSON.stringify(serverBalances));
// will send a null transaction to recalc because it will know what to do
recalculateBalances(serverBalances, transaction, function(){
// console.log(balancesObject);
binance.exchangeinfo(function(response) {
binance.getAllOpenOrders(function (orders) {
theToken.balance = balancesObject.usdt.availableBalance;
theToken.onOrder = balances[tether].onOrder;
theToken.minimumQuantity = 1;
theToken.quantityDecimals = 1;
theToken.priceDecimals = 2;
// iterate tether market pairs, e.g. btcusdt, bnbusdt
for (let i = 0; i < theToken.tokens.length; i++) {
let parent = theToken.tokens[i].token;
let thisPair = theToken.tokens[i].token + tether;
theToken.tokens[i].pair = thisPair;
theToken.tokens[i].parent = tether;
theToken.tokens[i].onOrder = balances[parent].onOrder;
if (theToken.tokens[i].token === 'btc'){
// console.log("setting btc balance (rest) to: " + balancesObject.btc.availableBalance);
theToken.tokens[i].balance = balancesObject.btc.availableBalance
}
else {
theToken.tokens[i].balance = balances[parent].available; // old way for non btc tokens
}
// poll exchange info for token specs
const elementPos = response.symbols.map(function (x) {
return x.symbol;
}).indexOf(thisPair);
const exchangeData = response.symbols[elementPos];
// add btc token specs to token object. The specs come in a "filters" object
for (let k = 0; k < exchangeData.filters.length - 1; k++) {
if (exchangeData.filters[k].filterType === 'LOT_SIZE') {
const minQ = exchangeData.filters[k].stepSize;
const stepSize = utilities.decimalPlaces(exchangeData.filters[k].stepSize);
theToken.tokens[i].quantityDecimals = stepSize;
theToken.tokens[i].minimumQuantity = minQ;
}
if (exchangeData.filters[k].filterType === 'PRICE_FILTER') {
const minPrice = utilities.decimalPlaces(exchangeData.filters[k].minPrice);
theToken.tokens[i].priceDecimals = minPrice;
}
if (exchangeData.filters[k].filterType === 'MIN_NOTIONAL') {
const minNotional = utilities.decimalPlaces(exchangeData.filters[k].minNotional);
// console.log("Minimum Notional: " + minNotional)
// not sure why is not added, perhaps it was an earlier thing
}
}
// iterate child tokens of btc level tokens
for (let j = 0; j < theToken.tokens[i].tokens.length; j++) {
const alt = theToken.tokens[i].tokens[j];
const altPair = alt.token + parent;
theToken.tokens[i].tokens[j].pair = altPair;
theToken.tokens[i].tokens[j].parent = parent;
// Altcoin balances are allowed to pass through directly
theToken.tokens[i].tokens[j].balance = balances[alt.token].available;
theToken.tokens[i].tokens[j].onOrder = balances[alt.token].onOrder;
// poll exchange info for token specs
const elementPos = response.symbols.map(function (x) {
return x.symbol;
}).indexOf(altPair);
const exchangeData = response.symbols[elementPos];
// add alt token specs to token object. The specs come in a "filters" object
for (let l = 0; l < exchangeData.filters.length - 1; l++) {
if (exchangeData.filters[l].filterType === 'LOT_SIZE') {
const minQ = exchangeData.filters[l].stepSize;
const stepSize = utilities.decimalPlaces(exchangeData.filters[l].stepSize);
theToken.tokens[i].tokens[j].quantityDecimals = stepSize;
theToken.tokens[i].tokens[j].minimumQuantity = minQ;
}
if (exchangeData.filters[l].filterType === 'PRICE_FILTER') {
const minPrice = utilities.decimalPlaces(exchangeData.filters[l].minPrice);
theToken.tokens[i].tokens[j].priceDecimals = minPrice;
}
}
}
}
tokenObj = theToken;
cb(tokenObj);
if (transaction === null){
// if null transaction found, need to reconcileUnknownTransaction();
// but want a fresh balances and token object to perform
reconcileUnknownTransaction();
}
});
});
});
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment