@debut/plugin-virtual-takes
Version:
Classic virtual takes and stops in percent
234 lines • 9.68 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.virtualTakesPlugin = void 0;
function virtualTakesPlugin(opts) {
const lookup = new Map();
const trailing = new Set();
const reducePrices = new Map();
let price = 0;
let ctx;
async function handleTick() {
// Нет заявки активной - нет мониторинга
if (!ctx.debut.ordersCount) {
return;
}
const orders = [...ctx.debut.orders];
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
if (!order || !('orderId' in order)) {
continue;
}
const hasTrailing = trailing.has(order.cid);
const { data, isLink } = getOrderData(order.cid, lookup, opts.separateStops);
const closeState = checkClose(order, price, lookup, opts.separateStops);
const moveTakeAfter = opts.trailing === 3 /* TrailingType.MoveAfterEachTake */ && closeState === 1 /* CloseType.TAKE */;
const startTakeAfter = !hasTrailing && opts.trailing === 2 /* TrailingType.StartAfterTake */ && closeState === 1 /* CloseType.TAKE */;
const reducePrice = reducePrices.get(order.cid);
if (opts.reduceWhen && reducePrice) {
const shouldClose = checkReduce(order.type, price, reducePrice);
if (shouldClose) {
await ctx.debut.reduceOrder(order, opts.reduceSize || 0.5);
reducePrices.delete(order.cid);
}
}
// if (opts.reduceOnTrailingTake && (moveTakeAfter || startTakeAfter)) {
// const originalData = lookup.get(order.cid)!;
// if (!originalData.reduced) {
// await ctx.debut.reduceOrder(order, 0.5);
// originalData.reduced = true;
// }
// }
if (isLink) {
continue;
}
// Update trailings and next check close state
if (hasTrailing && opts.trailing !== 3 /* TrailingType.MoveAfterEachTake */) {
trailingTakes(order, price, lookup);
}
if (moveTakeAfter) {
createTrailingTakes(order, price, lookup, opts.separateStops);
trailing.add(order.cid);
}
else if (startTakeAfter) {
const priceDiff = order.price - data.stopPrice;
data.stopPrice = price - priceDiff;
data.takePrice = price + priceDiff;
data.price = price;
trailing.add(order.cid);
}
else if (closeState === 0 /* CloseType.STOP */ && data.tryLeft > 0 && data.tryPrice && !data.trailed) {
const priceDiff = price - data.tryPrice;
data.stopPrice = data.stopPrice + priceDiff;
data.takePrice = data.takePrice + priceDiff;
data.tryPrice = price;
data.price = price;
// Create same type as origin order
const newOrder = await ctx.debut.createOrder(order.type);
// Error cannot be opened fallback
if (!newOrder) {
await ctx.debut.closeAll(true);
return;
}
lookup.set(newOrder.cid, { ...data, tryLeft: undefined, retryFor: order.cid });
data.tryLeft--;
}
else if (closeState === 0 /* CloseType.STOP */ || closeState === 1 /* CloseType.TAKE */) {
if (opts.maxRetryOrders && data.tryLeft < opts.maxRetryOrders) {
await ctx.debut.closeAll(true);
return;
}
else {
await ctx.debut.closeOrder(order);
}
}
}
}
return {
name: 'takes',
api: {
updateUpts(update) {
opts = { ...opts, ...update };
},
setTrailingForOrder(cid, takePrice, stopPrice) {
if (!opts.manual) {
throw 'Virtual Takes Plugin should be in a manual mode for call `setForOrder`';
}
if (opts.trailing === 1 /* TrailingType.Classic */) {
trailing.add(cid);
}
lookup.set(cid, { price, stopPrice, takePrice });
},
setPricesForOrder(cid, takePrice, stopPrice) {
if (!opts.manual) {
throw 'Virtual Takes Plugin should be in a manual mode for call `setForOrder`';
}
if (!takePrice || !stopPrice) {
throw `prices in setForOrder() should be a number, current take: ${takePrice}, stop: ${stopPrice}`;
}
// Only for orders seted up using API
lookup.set(cid, { price, takePrice, stopPrice, tryLeft: opts.maxRetryOrders, tryPrice: price });
if (opts.trailing === 1 /* TrailingType.Classic */) {
trailing.add(cid);
}
},
setForOrder(cid, type) {
if (!opts.manual) {
throw 'Virtual Takes Plugin should be in a manual mode for call `setForOrder`';
}
createTakes(cid, type, price, opts, lookup);
if (opts.trailing === 1 /* TrailingType.Classic */) {
trailing.add(cid);
}
},
getTakes(cid) {
return getOrderData(cid, lookup).data;
},
isManual() {
return opts.manual || false;
},
},
onInit() {
ctx = this;
},
async onOpen(order) {
if (opts.reduceWhen) {
const rev = order.type === "SELL" /* OrderType.SELL */ ? -1 : 1;
const reducePrice = price + rev * price * (opts.reduceWhen / 100);
reducePrices.set(order.cid, reducePrice);
}
if (opts.manual) {
return;
}
createTakes(order.cid, order.type, order.price, opts, lookup);
if (opts.trailing === 1 /* TrailingType.Classic */) {
trailing.add(order.cid);
}
},
async onClose(order, closing) {
if (!order.reduce) {
trailing.delete(closing.cid);
lookup.delete(closing.cid);
}
},
async onCandle() {
if (opts.ignoreTicks) {
await handleTick();
}
},
async onTick(tick) {
price = tick.c;
if (!opts.ignoreTicks) {
await handleTick();
}
},
};
}
exports.virtualTakesPlugin = virtualTakesPlugin;
function getOrderData(cid, lookup, separateStops) {
let data = lookup.get(cid);
let isLink = false;
if (!separateStops && data?.retryFor) {
data = lookup.get(data.retryFor);
isLink = true;
}
return { data: data || {}, isLink };
}
/**
* Проверяем достижение тейка на оснвании текущей цены
*/
function checkClose(order, price, lookup, separateStops) {
const { type, cid } = order;
const { takePrice, stopPrice } = getOrderData(cid, lookup, separateStops).data;
if ((type === "BUY" /* OrderType.BUY */ && price >= takePrice) || (type === "SELL" /* OrderType.SELL */ && price <= takePrice)) {
return 1 /* CloseType.TAKE */;
}
if ((type === "BUY" /* OrderType.BUY */ && price <= stopPrice) || (type === "SELL" /* OrderType.SELL */ && price >= stopPrice)) {
return 0 /* CloseType.STOP */;
}
return;
}
/**
* Order reduce price achieved
*/
function checkReduce(type, price, reducePrice) {
if ((type === "BUY" /* OrderType.BUY */ && price >= reducePrice) || (type === "SELL" /* OrderType.SELL */ && price <= reducePrice)) {
return true;
}
return false;
}
function createTakes(cid, type, price, opts, lookup) {
const rev = type === "SELL" /* OrderType.SELL */ ? -1 : 1;
if (!opts.stopLoss || !opts.takeProfit) {
throw new Error('Virtual Takes Plugin needs stopLoss and takeProfit in strategy options');
}
// XXX Так как тейки и стопы виртуальные, можем их не делать реальными ценами с шагом
const stopPrice = price - rev * price * (opts.stopLoss / 100);
const takePrice = price + rev * price * (opts.takeProfit / 100);
lookup.set(cid, { stopPrice, takePrice, price });
}
function createTrailingTakes(order, price, lookup, separateStops) {
const takes = getOrderData(order.cid, lookup, separateStops).data;
if (!takes) {
return;
}
const delta = (price - takes.stopPrice) / 2;
takes.takePrice += delta;
takes.stopPrice += delta;
takes.trailed = true;
}
function trailingTakes(order, price, lookup) {
const takes = getOrderData(order.cid, lookup).data;
const prevPrice = takes.price;
if (!takes.trailed) {
takes.price = price;
takes.trailed = true;
return;
}
takes.takePrice = order.type === "BUY" /* OrderType.BUY */ ? Infinity : -Infinity;
if ((order.type === "BUY" /* OrderType.BUY */ && price > prevPrice) || (order.type === "SELL" /* OrderType.SELL */ && price < prevPrice)) {
const delta = price - prevPrice;
takes.stopPrice += delta;
takes.price = price;
}
}
//# sourceMappingURL=index.js.map