Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/integration/exchange/services/exchange.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export abstract class ExchangeService extends PricingProvider implements OnModul
return this.markets;
}

private async getMinTradeAmount(pair: string): Promise<number> {
async getMinTradeAmount(pair: string): Promise<number> {
return this.getMarket(pair).then((m) => m.limits.amount.min);
}

Expand Down Expand Up @@ -294,6 +294,28 @@ export abstract class ExchangeService extends PricingProvider implements OnModul
return Util.roundToValue(price, pricePrecision);
}

async getBestBidLiquidity(from: string, to: string): Promise<{ price: number; amount: number } | undefined> {
const { pair, direction } = await this.getTradePair(from, to);

const minAmount = await this.getMinTradeAmount(pair);
const orderBook = await this.callApi((e) => e.fetchOrderBook(pair));
const { price: pricePrecision } = await this.getPrecision(pair);

const orders = direction === OrderSide.SELL ? orderBook.bids : orderBook.asks;

// Find first order that meets minimum amount requirement
const validOrder = orders.find(([, amount]) => amount >= minAmount);

if (!validOrder) return undefined;

const [price, amount] = validOrder;

return {
price: Util.roundToValue(price, pricePrecision),
amount,
};
}

// orders

protected async trade(from: string, to: string, amount: number): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
}

private async buy(order: LiquidityManagementOrder): Promise<CorrelationId> {
const { asset, tradeAsset, minTradeAmount, fullTrade } = this.parseBuyParams(order.action.paramMap);
const { asset, tradeAsset, minTradeAmount, fullTrade, liquidityLimited, maxPriceDeviation } = this.parseBuyParams(
order.action.paramMap,
);

const targetAssetEntity = asset
? await this.assetService.getAssetByUniqueName(asset)
Expand All @@ -139,18 +141,29 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
);
}

const price = await this.getAndCheckTradePrice(tradeAssetEntity, targetAssetEntity);
const price = await this.getAndCheckTradePrice(tradeAssetEntity, targetAssetEntity, maxPriceDeviation);

const minSellAmount = minTradeAmount ?? Util.floor(minAmount * price, 6);
const maxSellAmount = Util.floor(maxAmount * price, 6);

const availableBalance = await this.getAvailableTradeBalance(tradeAsset, targetAssetEntity.name);
if (minSellAmount > availableBalance)
let effectiveMax = Math.min(maxSellAmount, availableBalance);

if (liquidityLimited) {
const { amount: liquidity, price: liquidityPrice } = await this.getBestPriceLiquidity(
tradeAsset,
targetAssetEntity.name,
);
effectiveMax = Math.min(effectiveMax, Util.floor(liquidity * liquidityPrice, 6));
}

if (effectiveMax < minSellAmount) {
throw new OrderNotProcessableException(
`${this.exchangeService.name}: not enough balance for ${tradeAsset} (balance: ${availableBalance}, min. requested: ${minSellAmount}, max. requested: ${maxSellAmount})`,
`${this.exchangeService.name}: not enough balance/liquidity for ${tradeAsset} (balance: ${effectiveMax}, min. requested: ${minSellAmount}, max. requested: ${maxSellAmount})`,
);
}

const amount = Math.min(maxSellAmount, availableBalance);
const amount = effectiveMax;

order.inputAmount = amount;
order.inputAsset = tradeAsset;
Expand All @@ -174,20 +187,28 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
}

private async sell(order: LiquidityManagementOrder): Promise<CorrelationId> {
const { tradeAsset } = this.parseSellParams(order.action.paramMap);
const { tradeAsset, liquidityLimited, maxPriceDeviation } = this.parseSellParams(order.action.paramMap);

const asset = order.pipeline.rule.targetAsset.dexName;

const tradeAssetEntity = await this.assetService.getAssetByUniqueName(`${this.exchangeService.name}/${tradeAsset}`);
await this.getAndCheckTradePrice(order.pipeline.rule.targetAsset, tradeAssetEntity);
await this.getAndCheckTradePrice(order.pipeline.rule.targetAsset, tradeAssetEntity, maxPriceDeviation);

const availableBalance = await this.getAvailableTradeBalance(asset, tradeAsset);
if (order.minAmount > availableBalance)
let effectiveMax = Math.min(order.maxAmount, availableBalance);

if (liquidityLimited) {
const { amount: liquidity } = await this.getBestPriceLiquidity(asset, tradeAsset);
effectiveMax = Math.min(effectiveMax, liquidity);
}

if (effectiveMax < order.minAmount) {
throw new OrderNotProcessableException(
`${this.exchangeService.name}: not enough balance for ${asset} (balance: ${availableBalance}, min. requested: ${order.minAmount}, max. requested: ${order.maxAmount})`,
`${this.exchangeService.name}: not enough balance/liquidity for ${asset} (balance: ${effectiveMax}, min. requested: ${order.minAmount}, max. requested: ${order.maxAmount})`,
);
}

const amount = Math.min(order.maxAmount, availableBalance);
const amount = effectiveMax;

order.inputAmount = amount;
order.inputAsset = asset;
Expand All @@ -208,15 +229,15 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
}
}

private async getAndCheckTradePrice(from: Asset, to: Asset): Promise<number> {
private async getAndCheckTradePrice(from: Asset, to: Asset, maxPriceDeviation = 0.05): Promise<number> {
const price = await this.exchangeService.getCurrentPrice(from.name, to.name);

// price fetch should already throw error if out of range
const checkPrice = await this.pricingService.getPrice(from, to, PriceValidity.VALID_ONLY);

if (Math.abs((price - checkPrice.price) / checkPrice.price) > 0.05)
if (Math.abs((price - checkPrice.price) / checkPrice.price) > maxPriceDeviation)
throw new OrderFailedException(
`Trade price out of range: exchange price ${price}, check price ${checkPrice.price}`,
`Trade price out of range: exchange price ${price}, check price ${checkPrice.price}, max deviation ${maxPriceDeviation}`,
);

return price;
Expand Down Expand Up @@ -454,15 +475,19 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
tradeAsset: string;
minTradeAmount: number;
fullTrade: boolean;
liquidityLimited: boolean;
maxPriceDeviation?: number;
} {
const asset = params.asset as string | undefined;
const tradeAsset = params.tradeAsset as string | undefined;
const minTradeAmount = params.minTradeAmount as number | undefined;
const fullTrade = Boolean(params.fullTrade); // use full trade for directly triggered actions
const liquidityLimited = Boolean(params.liquidityLimited);
const maxPriceDeviation = params.maxPriceDeviation as number | undefined;

if (!tradeAsset) throw new Error(`Params provided to CcxtExchangeAdapter.buy(...) command are invalid.`);

return { asset, tradeAsset, minTradeAmount, fullTrade };
return { asset, tradeAsset, minTradeAmount, fullTrade, liquidityLimited, maxPriceDeviation };
}

private validateSellParams(params: Record<string, unknown>): boolean {
Expand All @@ -474,12 +499,18 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
}
}

private parseSellParams(params: Record<string, unknown>): { tradeAsset: string } {
private parseSellParams(params: Record<string, unknown>): {
tradeAsset: string;
liquidityLimited: boolean;
maxPriceDeviation?: number;
} {
const tradeAsset = params.tradeAsset as string | undefined;
const liquidityLimited = Boolean(params.liquidityLimited);
const maxPriceDeviation = params.maxPriceDeviation as number | undefined;

if (!tradeAsset) throw new Error(`Params provided to CcxtExchangeAdapter.sell(...) command are invalid.`);

return { tradeAsset };
return { tradeAsset, liquidityLimited, maxPriceDeviation };
}

private validateTransferParams(params: Record<string, unknown>): boolean {
Expand Down Expand Up @@ -516,4 +547,16 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
e.message?.includes(m),
);
}

private async getBestPriceLiquidity(from: string, to: string): Promise<{ amount: number; price: number }> {
const liquidity = await this.exchangeService.getBestBidLiquidity(from, to);

if (!liquidity) {
throw new OrderNotProcessableException(
`${this.exchangeService.name}: not enough liquidity for ${from} (no order in orderbook meets exchange minimum)`,
);
}

return liquidity;
}
}
Loading