Multi-asset: coupling process

This commit is contained in:
sairajzero 2022-02-04 02:28:02 +05:30
parent dbad25044a
commit 92caef2c37
4 changed files with 89 additions and 66 deletions

View File

@ -47,6 +47,7 @@ FOREIGN KEY (floID) REFERENCES Users(floID)
CREATE TABLE SellOrder (
id INT NOT NULL AUTO_INCREMENT,
floID CHAR(34) NOT NULL,
asset VARCHAR(64) NOT NULL,
quantity FLOAT NOT NULL,
minPrice DECIMAL(10, 2),
time_placed DATETIME DEFAULT CURRENT_TIMESTAMP,
@ -57,6 +58,7 @@ FOREIGN KEY (floID) REFERENCES Users(floID)
CREATE TABLE BuyOrder (
id INT NOT NULL AUTO_INCREMENT,
floID CHAR(34) NOT NULL,
asset VARCHAR(64) NOT NULL,
quantity FLOAT NOT NULL,
maxPrice DECIMAL(10, 2) NOT NULL,
time_placed DATETIME DEFAULT CURRENT_TIMESTAMP,
@ -67,6 +69,7 @@ FOREIGN KEY (floID) REFERENCES Users(floID)
CREATE TABLE Transactions (
seller CHAR(34) NOT NULL,
buyer CHAR(34) NOT NULL,
asset VARCHAR(64) NOT NULL,
quantity FLOAT NOT NULL,
unitValue DECIMAL(10, 2) NOT NULL,
tx_time DATETIME DEFAULT CURRENT_TIMESTAMP,

View File

@ -3,11 +3,16 @@
const group = require("./group");
const price = require("./price");
var DB; //container for database
var DB, assetList; //container for database and assetList
function initiate() {
price.getRates().then(cur_rate => {
group.getBestPairs(cur_rate)
for (let asset in assetList)
startCouplingForAsset(asset);
}
function startCouplingForAsset(asset) {
price.getRates(asset).then(cur_rate => {
group.getBestPairs(asset, cur_rate)
.then(bestPairQueue => processCoupling(bestPairQueue))
.catch(error => console.error("initiateCoupling", error))
}).catch(error => console.error(error));
@ -19,14 +24,14 @@ function processCoupling(bestPairQueue) {
seller_best = pair_result.sellOrder;
console.debug("Sell:", seller_best);
console.debug("Buy:", buyer_best);
spendFLO(buyer_best, seller_best, pair_result.null_base).then(spend_result => {
spendAsset(bestPairQueue.asset, buyer_best, seller_best, pair_result.null_base).then(spend_result => {
let tx_quantity = spend_result.quantity,
txQueries = spend_result.txQueries,
clear_sell = spend_result.incomplete && !spend_result.flag_baseNull; //clear_sell can be true only if an order is placed without enough FLO
clear_sell = spend_result.incomplete && !spend_result.flag_baseNull; //clear_sell can be true only if an order is placed without enough ASSET
processOrders(seller_best, buyer_best, txQueries, tx_quantity, clear_sell);
updateBalance(seller_best, buyer_best, txQueries, bestPairQueue.cur_rate, tx_quantity);
updateBalance(seller_best, buyer_best, txQueries, bestPairQueue.asset, bestPairQueue.cur_rate, tx_quantity);
//begin audit
beginAudit(seller_best.floID, buyer_best.floID, bestPairQueue.cur_rate, tx_quantity).then(audit => {
beginAudit(seller_best.floID, buyer_best.floID, bestPairQueue.asset, bestPairQueue.cur_rate, tx_quantity).then(audit => {
//process txn query in SQL
DB.transaction(txQueries).then(_ => {
bestPairQueue.next(tx_quantity, spend_result.incomplete, spend_result.flag_baseNull);
@ -58,13 +63,13 @@ function processCoupling(bestPairQueue) {
console.log("No valid sellOrders.");
noSell = true;
}
price.noOrder(noBuy, noSell);
price.noOrder(bestPairQueue.asset, noBuy, noSell);
});
}
function spendFLO(buyOrder, sellOrder, null_base) {
function spendAsset(asset, buyOrder, sellOrder, null_base) {
return new Promise((resolve, reject) => {
DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY base", [sellOrder.floID]).then(result => {
DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? AND asset=? ORDER BY base", [sellOrder.floID, asset]).then(result => {
let rem = Math.min(buyOrder.quantity, sellOrder.quantity),
txQueries = [],
flag_baseNull = false;
@ -104,49 +109,50 @@ function processOrders(seller_best, buyer_best, txQueries, quantity, clear_sell)
txQueries.push(["UPDATE SellOrder SET quantity=quantity-? WHERE id=?", [quantity, seller_best.id]]);
}
function updateBalance(seller_best, buyer_best, txQueries, cur_price, quantity) {
//Update rupee balance for seller and buyer
function updateBalance(seller_best, buyer_best, txQueries, asset, cur_price, quantity) {
//Update cash balance for seller and buyer
let totalAmount = cur_price * quantity;
txQueries.push(["UPDATE Cash SET rupeeBalance=rupeeBalance+? WHERE floID=?", [totalAmount, seller_best.floID]]);
txQueries.push(["UPDATE Cash SET rupeeBalance=rupeeBalance-? WHERE floID=?", [totalAmount, buyer_best.floID]]);
txQueries.push(["UPDATE Cash SET balance=balance+? WHERE floID=?", [totalAmount, seller_best.floID]]);
txQueries.push(["UPDATE Cash SET balance=balance-? WHERE floID=?", [totalAmount, buyer_best.floID]]);
//Add coins to Buyer
txQueries.push(["INSERT INTO Vault(floID, base, quantity) VALUES (?, ?, ?)", [buyer_best.floID, cur_price, quantity]])
txQueries.push(["INSERT INTO Vault(floID, asset, base, quantity) VALUES (?, ?, ?, ?)", [buyer_best.floID, asset, cur_price, quantity]])
//Record transaction
txQueries.push(["INSERT INTO Transactions (seller, buyer, quantity, unitValue) VALUES (?, ?, ?, ?)", [seller_best.floID, buyer_best.floID, quantity, cur_price]]);
txQueries.push(["INSERT INTO Transactions (seller, buyer, asset, quantity, unitValue) VALUES (?, ?, ?, ?, ?)", [seller_best.floID, buyer_best.floID, asset, quantity, cur_price]]);
}
function beginAudit(sellerID, buyerID, unit_price, quantity) {
function beginAudit(sellerID, buyerID, asset, unit_price, quantity) {
return new Promise((resolve, reject) => {
auditBalance(sellerID, buyerID).then(old_bal => resolve({
end: () => endAudit(sellerID, buyerID, old_bal, unit_price, quantity)
auditBalance(sellerID, buyerID, asset).then(old_bal => resolve({
end: () => endAudit(sellerID, buyerID, asset, old_bal, unit_price, quantity)
})).catch(error => reject(error))
})
}
function endAudit(sellerID, buyerID, old_bal, unit_price, quantity) {
auditBalance(sellerID, buyerID).then(new_bal => {
DB.query("INSERT INTO auditTransaction (sellerID, buyerID, quantity, unit_price, total_cost, " +
" Rupee_seller_old, Rupee_seller_new, Rupee_buyer_old, Rupee_buyer_new," +
" FLO_seller_old, FLO_seller_new, FLO_buyer_old, FLO_buyer_new) " +
" Value (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [sellerID, buyerID, quantity, unit_price, quantity * unit_price,
old_bal[sellerID].Rupee, new_bal[sellerID].Rupee, old_bal[buyerID].Rupee, new_bal[buyerID].Rupee,
old_bal[sellerID].FLO, new_bal[sellerID].FLO, old_bal[buyerID].FLO, new_bal[buyerID].FLO,
function endAudit(sellerID, buyerID, asset, old_bal, unit_price, quantity) {
auditBalance(sellerID, buyerID, asset).then(new_bal => {
DB.query("INSERT INTO auditTransaction (asset, quantity, unit_price, total_cost," +
" sellerID, seller_old_cash, seller_old_asset, seller_new_cash, seller_new_asset," +
" buyerID, buyer_old_cash, buyer_old_asset, buyer_new_cash, buyer_new_asset)" +
" Value (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
asset, quantity, unit_price, quantity * unit_price,
sellerID, old_bal[sellerID].cash, old_bal[sellerID].asset, new_bal[sellerID].cash, new_bal[sellerID].asset,
buyerID, old_bal[buyerID].cash, old_bal[buyerID].asset, new_bal[buyerID].cash, new_bal[buyerID].asset,
]).then(_ => null).catch(error => console.error(error))
}).catch(error => console.error(error));
}
function auditBalance(sellerID, buyerID) {
function auditBalance(sellerID, buyerID, asset) {
return new Promise((resolve, reject) => {
let balance = {
[sellerID]: {},
[buyerID]: {}
};
DB.query("SELECT floID, rupeeBalance FROM Cash WHERE floID IN (?, ?)", [sellerID, buyerID]).then(result => {
DB.query("SELECT floID, balance FROM Cash WHERE floID IN (?, ?)", [sellerID, buyerID]).then(result => {
for (let i in result)
balance[result[i].floID].Rupee = result[i].rupeeBalance;
DB.query("SELECT floID, SUM(quantity) as floBal FROM Vault WHERE floID IN (?, ?) GROUP BY floID", [sellerID, buyerID]).then(result => {
balance[result[i].floID].cash = result[i].balance;
DB.query("SELECT floID, SUM(quantity) as asset_balance FROM Vault WHERE asset=? AND floID IN (?, ?) GROUP BY floID", [asset, sellerID, buyerID]).then(result => {
for (let i in result)
balance[result[i].floID].FLO = result[i].floBal;
balance[result[i].floID].asset = result[i].asset_balance;
resolve(balance);
}).catch(error => reject(error))
}).catch(error => reject(error))
@ -155,11 +161,17 @@ function auditBalance(sellerID, buyerID) {
module.exports = {
initiate,
group,
group: {
addTag: group.addTag,
removeTag: group.removeTag
},
price,
set DB(db) {
DB = db;
group.DB = db;
price.DB = db;
},
set assetList(list) {
assetList = list;
}
}

View File

@ -25,22 +25,25 @@ function removeTag(floID, tag) {
})
}
function getBestPairs(currentRate) {
function getBestPairs(asset, cur_rate) {
return new Promise((resolve, reject) => {
DB.query("SELECT tag, sellPriority, buyPriority FROM TagList").then(result => {
//Sorted in Ascending (ie, stack; pop for highest)
let tags_buy = result.sort((a, b) => a.buyPriority > b.buyPriority ? 1 : -1).map(r => r.tag);
let tags_sell = result.sort((a, b) => a.sellPriority > b.sellPriority ? 1 : -1).map(r => r.tag);
resolve(new bestPair(currentRate, tags_buy, tags_sell));
resolve(new bestPair(asset, cur_rate, tags_buy, tags_sell));
}).catch(error => reject(error))
})
}
const bestPair = function(cur_rate, tags_buy, tags_sell) {
const currentRate = cur_rate;
const bestPair = function(asset, cur_rate, tags_buy, tags_sell) {
Object.defineProperty(this, 'asset', {
get: () => asset
});
Object.defineProperty(this, 'cur_rate', {
get: () => currentRate
get: () => cur_rate,
});
this.get = () => new Promise((resolve, reject) => {
@ -89,7 +92,7 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) {
const getSellOrder = () => new Promise((resolve, reject) => {
let cache = getSellOrder.cache;
if (cache.cur_order) { //If cache already has a pending order
verifySellOrder(cache.cur_order, currentRate, cache.mode_null).then(result => {
verifySellOrder(cache.cur_order, asset, cur_rate, cache.mode_null).then(result => {
cache.cur_order = result;
resolve(result);
}).catch(error => {
@ -102,7 +105,7 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) {
.catch(error => reject(error))
})
} else if (cache.orders && cache.orders.length) { //If cache already has orders in priority
getTopValidSellOrder(cache.orders, currentRate, cache.mode_null).then(result => {
getTopValidSellOrder(cache.orders, asset, cur_rate, cache.mode_null).then(result => {
cache.cur_order = result;
resolve(result);
}).catch(error => {
@ -116,14 +119,14 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) {
})
} else if (cache.tags.length) { //If cache has remaining tags
cache.cur_tag = cache.tags.pop();
getSellOrdersInTag(cache.cur_tag, currentRate).then(orders => {
getSellOrdersInTag(cache.cur_tag, asset, cur_rate).then(orders => {
cache.orders = orders;
getSellOrder()
.then(result => resolve(result))
.catch(error => reject(error))
}).catch(error => reject(error));
} else if (!cache.end) { //Un-tagged floID's orders (do only once)
getUntaggedSellOrders(currentRate).then(orders => {
getUntaggedSellOrders(asset, cur_rate).then(orders => {
cache.orders = orders;
cache.cur_tag = null;
cache.end = true;
@ -150,7 +153,7 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) {
const getBuyOrder = () => new Promise((resolve, reject) => {
let cache = getBuyOrder.cache;
if (cache.cur_order) { //If cache already has a pending order
verifyBuyOrder(cache.cur_order, currentRate).then(result => {
verifyBuyOrder(cache.cur_order, cur_rate).then(result => {
cache.cur_order = result;
resolve(result);
}).catch(error => {
@ -163,7 +166,7 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) {
.catch(error => reject(error))
})
} else if (cache.orders && cache.orders.length) { //If cache already has orders in priority
getTopValidBuyOrder(cache.orders, currentRate).then(result => {
getTopValidBuyOrder(cache.orders, cur_rate).then(result => {
cache.cur_order = result;
resolve(result);
}).catch(error => {
@ -177,14 +180,14 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) {
})
} else if (cache.tags.length) { //If cache has remaining tags
cache.cur_tag = cache.tags.pop();
getBuyOrdersInTag(cache.cur_tag, currentRate).then(orders => {
getBuyOrdersInTag(cache.cur_tag, asset, cur_rate).then(orders => {
cache.orders = orders;
getBuyOrder()
.then(result => resolve(result))
.catch(error => reject(error))
}).catch(error => reject(error));
} else if (!cache.end) { //Un-tagged floID's orders (do only once)
getUntaggedBuyOrders(currentRate).then(orders => {
getUntaggedBuyOrders(asset, cur_rate).then(orders => {
cache.orders = orders;
cache.cur_tag = null;
cache.end = true;
@ -200,31 +203,34 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) {
};
}
function getUntaggedSellOrders(cur_price) {
function getUntaggedSellOrders(asset, cur_price) {
return new Promise((resolve, reject) => {
DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" +
" LEFT JOIN Tags ON Tags.floID = SellOrder.floID" +
" WHERE Tags.floID IS NULL AND SellOrder.minPrice <=? ORDER BY SellOrder.time_placed DESC", [cur_price])
" WHERE Tags.floID IS NULL AND SellOrder.asset = ? AND SellOrder.minPrice <=?" +
" ORDER BY SellOrder.time_placed DESC", [asset, cur_price])
.then(orders => resolve(orders))
.catch(error => reject(error))
})
}
function getUntaggedBuyOrders(cur_price) {
function getUntaggedBuyOrders(asset, cur_price) {
return new Promise((resolve, reject) => {
DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" +
" LEFT JOIN Tags ON Tags.floID = BuyOrder.floID" +
" WHERE Tags.floID IS NULL AND BuyOrder.maxPrice >=? ORDER BY BuyOrder.time_placed DESC", [cur_price])
" WHERE Tags.floID IS NULL AND BuyOrder.asset = ? AND BuyOrder.maxPrice >=? " +
" ORDER BY BuyOrder.time_placed DESC", [asset, cur_price])
.then(orders => resolve(orders))
.catch(error => reject(error))
})
}
function getSellOrdersInTag(tag, cur_price) {
function getSellOrdersInTag(tag, asset, cur_price) {
return new Promise((resolve, reject) => {
DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" +
" INNER JOIN Tags ON Tags.floID = SellOrder.floID" +
" WHERE Tags.tag = ? AND SellOrder.minPrice <=? ORDER BY SellOrder.time_placed DESC", [tag, cur_price]).then(orders => {
" WHERE Tags.tag = ? AND SellOrder.asset = ? AND SellOrder.minPrice <=?" +
" ORDER BY SellOrder.time_placed DESC", [tag, asset, cur_price]).then(orders => {
if (orders.length <= 1) // No (or) Only-one order, hence priority sort not required.
resolve(orders);
else
@ -238,11 +244,12 @@ function getSellOrdersInTag(tag, cur_price) {
});
}
function getBuyOrdersInTag(tag, cur_price) {
function getBuyOrdersInTag(tag, asset, cur_price) {
return new Promise((resolve, reject) => {
DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" +
" INNER JOIN Tags ON Tags.floID = BuyOrder.floID" +
" WHERE Tags.tag = ? AND BuyOrder.maxPrice >=? ORDER BY BuyOrder.time_placed DESC", [tag, cur_price]).then(orders => {
" WHERE Tags.tag = ? AND BuyOrder.asset = ? AND BuyOrder.maxPrice >=?" +
" ORDER BY BuyOrder.time_placed DESC", [tag, asset, cur_price]).then(orders => {
if (orders.length <= 1) // No (or) Only-one order, hence priority sort not required.
resolve(orders);
else
@ -287,26 +294,26 @@ function fetch_api(api, id) {
})
}
function getTopValidSellOrder(orders, cur_price, mode_null) {
function getTopValidSellOrder(orders, asset, cur_price, mode_null) {
return new Promise((resolve, reject) => {
if (!orders.length)
return reject(false)
verifySellOrder(orders.pop(), cur_price, mode_null) //pop: as the orders are sorted in ascending (highest point should be checked 1st)
verifySellOrder(orders.pop(), asset, cur_price, mode_null) //pop: as the orders are sorted in ascending (highest point should be checked 1st)
.then(result => resolve(result))
.catch(error => {
if (error !== false)
return reject(error);
getTopValidSellOrder(orders, cur_price, mode_null)
getTopValidSellOrder(orders, asset, cur_price, mode_null)
.then(result => resolve(result))
.catch(error => reject(error));
});
});
}
function verifySellOrder(sellOrder, cur_price, mode_null) {
function verifySellOrder(sellOrder, asset, cur_price, mode_null) {
return new Promise((resolve, reject) => {
if (!mode_null)
DB.query("SELECT quantity, base FROM Vault WHERE floID=? AND base IS NOT NULL ORDER BY base", [sellOrder.floID]).then(result => {
DB.query("SELECT quantity, base FROM Vault WHERE floID=? AND asset=? AND base IS NOT NULL ORDER BY base", [sellOrder.floID, asset]).then(result => {
let rem = sellOrder.quantity,
sell_base = 0,
base_quantity = 0;
@ -329,7 +336,7 @@ function verifySellOrder(sellOrder, cur_price, mode_null) {
resolve(sellOrder);
}).catch(error => reject(error));
else if (mode_null)
DB.query("SELECT SUM(quantity) as total FROM Vault WHERE floID=?", [sellOrder.floID]).then(result => {
DB.query("SELECT SUM(quantity) as total FROM Vault WHERE floID=? AND asset=?", [sellOrder.floID, asset]).then(result => {
if (result.total < sellOrder.quantity)
console.warn(`Sell Order ${sellOrder.id} was made without enough FLO. This should not happen`);
if (result.total > 0)
@ -358,10 +365,10 @@ function getTopValidBuyOrder(orders, cur_price) {
function verifyBuyOrder(buyOrder, cur_price) {
return new Promise((resolve, reject) => {
DB.query("SELECT rupeeBalance AS bal FROM Cash WHERE floID=?", [buyOrder.floID]).then(result => {
DB.query("SELECT balance AS bal FROM Cash WHERE floID=?", [buyOrder.floID]).then(result => {
if (result[0].bal < cur_price * buyOrder.quantity) {
//This should not happen unless a buy order is placed when user doesnt have enough rupee balance
console.warn(`Buy order ${buyOrder.id} is active, but rupee# is insufficient`);
//This should not happen unless a buy order is placed when user doesnt have enough cash balance
console.warn(`Buy order ${buyOrder.id} is active, but Cash is insufficient`);
reject(false);
} else
resolve(buyOrder);

View File

@ -67,7 +67,7 @@ function addSellOrder(floID, asset, quantity, min_price) {
return reject(INVALID(`Invalid asset (${asset})`));
getAssetBalance.check(floID, asset, quantity).then(_ => {
checkSellRequirement(floID).then(_ => {
DB.query("INSERT INTO SellOrder(floID, quantity, minPrice) VALUES (?, ?, ?)", [floID, quantity, min_price])
DB.query("INSERT INTO SellOrder(floID, asset, quantity, minPrice) VALUES (?, ?, ?, ?)", [floID, asset, quantity, min_price])
.then(result => resolve("Added SellOrder to DB"))
.catch(error => reject(error));
}).catch(error => reject(error))
@ -100,7 +100,7 @@ function addBuyOrder(floID, asset, quantity, max_price) {
else if (!allowedAssets.includes(asset))
return reject(INVALID(`Invalid asset (${asset})`));
getAssetBalance.check(floID, asset, quantity).then(_ => {
DB.query("INSERT INTO BuyOrder(floID, quantity, maxPrice) VALUES (?, ?, ?)", [floID, quantity, max_price])
DB.query("INSERT INTO BuyOrder(floID, asset, quantity, maxPrice) VALUES (?, ?, ?, ?)", [floID, asset, quantity, max_price])
.then(result => resolve("Added BuyOrder to DB"))
.catch(error => reject(error));
}).catch(error => reject(error));
@ -462,6 +462,7 @@ module.exports = {
coupling.DB = db;
},
set allowedAssets(assets) {
allowedAssets = assets;
allowedAssets = Object.keys(assets);
coupling.assetList = assets;
}
};