gdax-sim
Version:
Simulator used to help unit test and back test various Coinbase-Pro (gdax) interactions.
415 lines (383 loc) • 12.7 kB
JavaScript
const WebocketSim = require("./WebsocketSim");
const UserSim = require("./UserAccountSim");
const GenerateOrder = require("./OrderGenerator");
const { createMatch, createMatchesFromCandle } = require("./MatchGenerators");
const HistoricRates = require("./HistoricRates");
const GetOrder = require("./Getorder");
const CompleteOrder = require("./CompleteOrder");
const EventDriver = require("../Lib/EventDriver");
class ApiSim {
constructor(_params) {
//base-currency / quote-currency
let params = _params === undefined ? {} : _params;
this.eventDriver = new EventDriver();
this.user = new UserSim();
this.user.cryptoBalance = isNaN(params.base_balance)
? 100
: params.base_balance;
this.user.fiatBalance = isNaN(params.quote_balance)
? 100
: params.quote_balance;
this.hour_start_on = isNaN(params.hour_start_on) ? 0 : params.hour_start_on;
this.websocketClient = new WebocketSim();
this.currentPrice = 0;
this.pair = "ETH-BTC";
this.currentTime = new Date().toISOString();
this.taker_fee = isNaN(params.taker_fee) ? 0.005 : params.taker_fee / 100;
this.historics = {
m1: [],
m5: [],
m15: [],
h1: [],
h6: [],
d1: [],
};
}
afterSession() {}
generateSalt() {
return new Date().getTime().toString();
}
completeOrder(order) {
return CompleteOrder.call(this, order);
}
getOrder(orderId, callback) {
GetOrder.call(this, orderId, callback);
}
getProductHistoricRates(product, params, callback) {
HistoricRates.getProduct.call(this, product, params, callback);
}
logHistoricData(message) {
HistoricRates.processMatch.call(this, message);
}
generateOrder(orderParams, salt) {
return GenerateOrder.call(this, orderParams, salt);
}
buy(buyParams, callback) {
buyParams.side = "buy";
this.createOrder(buyParams, callback);
}
sell(sellParams, callback) {
sellParams.side = "sell";
this.createOrder(sellParams, callback);
}
cancelOrder(orderId, callback) {
let data;
let order;
let buyIndex = this.user.limitOrders.openBuys
.map((e) => {
return e.id;
})
.indexOf(orderId);
let sellIndex = this.user.limitOrders.openSells
.map((e) => {
return e.id;
})
.indexOf(orderId);
if (buyIndex === -1 && sellIndex === -1) {
data = {
message: "no order by that id",
};
} else {
if (buyIndex !== -1) {
order = this.user.limitOrders.openBuys.splice(buyIndex, 1)[0];
this.user.fiatBalance +=
parseFloat(order.size) * parseFloat(order.price);
} else if (sellIndex !== -1) {
order = this.user.limitOrders.openSells.splice(sellIndex, 1)[0];
this.user.cryptoBalance += parseFloat(order.size);
}
data = order.id;
}
if (typeof callback === "function") {
callback(null, null, data);
}
}
//Below are supporting functions
backtest(candleData, options = {}) {
let messages = createMatchesFromCandle(
candleData,
options.start_time,
options.end_time,
this.pair,
options.reduceSignals,
);
messages.reverse();
let nextPrice, nextTime;
while (messages.length > 0) {
let m = messages.pop();
//market orders
if (this.user.orders.length >= 1) {
this.user.orders.forEach((o) => {
if (o.status === "pending") {
let newmsg = this.fillOrder(o.id, null, this.currentTime);
o.status = "done";
for (let i = newmsg.length - 1; i >= 0; i--) {
messages.push(newmsg[i]);
}
}
});
}
//update values
if (m.price !== undefined) {
this.currentPrice = parseFloat(m.price);
}
if (m.time !== undefined) {
this.currentTime = m.time;
}
//limit orders below
if (messages.length > 1) {
let newmsg = [];
let primeIndex = messages.length - 1;
let mPrime = messages[primeIndex];
if (mPrime.type === "match") {
nextPrice = parseFloat(mPrime.price);
nextTime = mPrime.time;
if (nextPrice < this.currentPrice) {
//buy order check
this.user.limitOrders.openBuys.forEach((value, index) => {
let orderPrice = parseFloat(value.price);
if (
value.status === "pending" &&
orderPrice > nextPrice &&
orderPrice <= this.currentPrice
) {
newmsg = this.fillOrder(
this.user.limitOrders.openBuys[index].id,
null,
this.avgTime(this.currentTime, nextTime),
);
}
});
} else if (nextPrice > this.currentPrice) {
//sellOrderCheck
this.user.limitOrders.openSells.forEach((value, index) => {
let orderPrice = parseFloat(value.price);
if (
value.status === "pending" &&
orderPrice < nextPrice &&
orderPrice >= this.currentPrice
) {
newmsg = this.fillOrder(
this.user.limitOrders.openSells[index].id,
null,
this.avgTime(this.currentTime, nextTime),
);
}
});
}
for (let i = newmsg.length - 1; i >= 0; i--) {
messages.push(newmsg[i]);
}
}
}
//disbatch the message as the final thing
this.logHistoricData(m);
this.websocketClient.disbatch("message", m);
}
if (typeof this.afterSession === "function") {
this.afterSession();
}
}
fillOrder(orderId, size, time) {
let order;
let messages = [];
let limitBuyIndex = this.user.limitOrders.openBuys
.map((e) => {
return e.id;
})
.indexOf(orderId);
let limitSellIndex = this.user.limitOrders.openSells
.map((e) => {
return e.id;
})
.indexOf(orderId);
let marketOrderIndex = this.user.orders
.map((e) => {
return e.id;
})
.indexOf(orderId);
if (limitBuyIndex !== -1 || limitSellIndex !== -1) {
if (limitBuyIndex !== -1) {
order = this.user.limitOrders.openBuys[limitBuyIndex];
this.user.cryptoBalance += parseFloat(order.size);
this.completeOrder(this.user.limitOrders.openBuys[limitBuyIndex]);
} else if (limitSellIndex !== -1) {
order = this.user.limitOrders.openSells[limitSellIndex];
this.user.fiatBalance +=
parseFloat(order.size) * parseFloat(order.price);
this.completeOrder(this.user.limitOrders.openSells[limitSellIndex]);
}
messages.push(
createMatch({
side: order.side,
maker_order_id: order.id,
size: order.size,
price: order.price,
product_id: order.product_id,
time: time,
isUser: 1,
}),
);
messages.push({
type: "done",
side: order.side,
order_id: orderId,
reason: "filled",
product_id: order.product_id,
price: order.price.toString(),
remaining_size: "0.00000000",
sequence: Math.round(100000000 * Math.random()),
time: time,
isUser: 1,
});
} else if (marketOrderIndex !== -1) {
if (this.user.orders[marketOrderIndex].status !== "done") {
this.user.orders[marketOrderIndex] = this.completeOrder(
this.user.orders[marketOrderIndex],
);
order = this.user.orders[marketOrderIndex];
if (order.side === "buy") {
let size = order.size === undefined ? order.filled_size : order.size;
let funds =
order.specified_funds !== undefined
? parseFloat(order.specified_funds)
: parseFloat(order.size) *
this.currentPrice *
(1 + this.taker_fee);
this.user.cryptoBalance += parseFloat(size);
this.user.fiatBalance -= funds;
//no size on markt buys with funds until they are completed
} else if (order.side === "sell") {
let funds =
order.size === undefined
? parseFloat(order.funds)
: parseFloat(order.size) *
this.currentPrice *
(1 - this.taker_fee);
this.user.fiatBalance += funds;
}
messages.push(
createMatch({
side: order.side,
taker_order_id: order.id,
size: order.size !== undefined ? order.size : order.filled_size,
price: this.currentPrice,
product_id: order.product_id,
time: time,
isUser: 1,
}),
);
messages.push({
type: "done",
side: order.side,
order_id: order.id,
reason: "filled",
product_id: order.product_id,
price: this.price,
remaining_size: "0.00000000",
sequence: Math.round(100000000 * Math.random()),
time: time,
isUser: 1,
});
if (messages[1].side === "buy")
this.eventDriver.onBuy(
this.user.fiatBalance,
this.user.cryptoBalance,
order,
);
else
this.eventDriver.onSell(
this.user.fiatBalance,
this.user.cryptoBalance,
order,
);
}
}
return messages;
}
createOrder(orderPerams, callback) {
let data = {
status: "rejected",
};
let orderPrice = parseFloat(orderPerams.price);
let orderSize = parseFloat(orderPerams.size);
let orderFunds = parseFloat(orderPerams.funds);
let order = this.generateOrder(orderPerams, this.generateSalt());
if (
!(
order.type === "limit" &&
order.side === "buy" &&
parseFloat(order.price) >= this.currentPrice
) &&
!(
order.type === "limit" &&
order.side === "buy" &&
parseFloat(order.price) * parseFloat(order.size) > this.user.fiatBalance
) &&
!(
order.type === "limit" &&
order.side === "sell" &&
parseFloat(order.price) <= this.currentPrice
) &&
!(
order.type === "market" &&
order.side === "buy" &&
this.currentPrice * parseFloat(order.size) * (1 + this.taker_fee) >
this.user.fiatBalance
) &&
!(
order.type === "market" &&
order.side === "buy" &&
orderFunds > this.user.fiatBalance
) &&
!(
order.type === "market" &&
order.side === "sell" &&
orderFunds > this.user.cryptoBalance * this.currentPrice
) &&
!(
order.side === "sell" &&
parseFloat(order.size) > this.user.cryptoBalance
)
/*&&
!(order.side === 'buy' && parseFloat(order.funds) > this.user.fiatBalance)*/
) {
//save order
if (order.type === "limit") {
if (order.side === "buy") {
this.user.limitOrders.openBuys.push(order);
this.user.fiatBalance -= orderPrice * orderSize;
} else {
this.user.limitOrders.openSells.push(order);
this.user.cryptoBalance -= orderSize;
}
} else if (order.type === "market") {
if (order.side === "buy") {
if (!isNaN(orderFunds)) {
//order.size = (orderFunds * (1 - this.taker_fee)) / this.currentPrice).toString()
}
this.user.orders.push(order);
} else {
if (!isNaN(orderFunds)) {
orderSize = orderFunds / this.currentPrice;
//order.size = orderSize.toString();
}
this.user.cryptoBalance -= orderSize;
this.user.orders.push(order);
}
}
//set data to order for callback
data = order;
}
if (typeof callback === "function") {
callback(null, null, data);
}
}
avgTime(t1, t2) {
let d1 = new Date(t1).getTime();
let d2 = new Date(t2).getTime();
let avg = (d1 + d2) / 2;
return new Date(avg).toISOString();
}
}
module.exports = ApiSim;