Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding new flag to trade all possible quantity for buy/sell #1922

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions docs/api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 17 additions & 5 deletions lib/cli/placeorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { checkDecimalPlaces } from '../utils/utils';
export const placeOrderBuilder = (argv: Argv, side: OrderSide) => {
const command = side === OrderSide.BUY ? 'buy' : 'sell';
argv.positional('quantity', {
type: 'number',
describe: 'the quantity to trade',
type: 'string',
describe: 'the quantity to trade, `max` trades everything',
})
.positional('pair_id', {
type: 'string',
Expand Down Expand Up @@ -39,6 +39,8 @@ export const placeOrderBuilder = (argv: Argv, side: OrderSide) => {
describe: 'immediate-or-cancel',
})
.example(`$0 ${command} 5 LTC/BTC .01 1337`, `place a limit order to ${command} 5 LTC @ 0.01 BTC with local order id 1337`)
.example(`$0 ${command} max LTC/BTC .01`, `place a limit order to ${command} max LTC @ 0.01 BTC`)
.example(`$0 ${command} max BTC/USDT mkt`, `place a market order to ${command} max BTC for USDT`)
.example(`$0 ${command} 3 BTC/USDT mkt`, `place a market order to ${command} 3 BTC for USDT`)
.example(`$0 ${command} 1 BTC/USDT market`, `place a market order to ${command} 1 BTC for USDT`);
};
Expand All @@ -48,9 +50,17 @@ export const placeOrderHandler = async (argv: Arguments<any>, side: OrderSide) =

const numericPrice = Number(argv.price);
const priceStr = argv.price.toLowerCase();
const isMax = argv.quantity === 'max';

const quantity = coinsToSats(argv.quantity);
request.setQuantity(quantity);
if (isMax) {
request.setMax(true);
} else {
if (isNaN(argv.quantity)) {
console.error('quantity is not a valid number');
process.exit(1);
}
request.setQuantity(coinsToSats(parseFloat(argv.quantity)));
}
request.setSide(side);
request.setPairId(argv.pair_id.toUpperCase());
request.setImmediateOrCancel(argv.ioc);
Expand Down Expand Up @@ -81,7 +91,7 @@ export const placeOrderHandler = async (argv: Arguments<any>, side: OrderSide) =
} else {
const subscription = client.placeOrder(request);
let noMatches = true;
let remainingQuantity = quantity;
let remainingQuantity = isMax ? 0 : coinsToSats(parseFloat(argv.quantity));
subscription.on('data', (response: PlaceOrderEvent) => {
if (argv.json) {
console.log(JSON.stringify(response.toObject(), undefined, 2));
Expand Down Expand Up @@ -113,6 +123,8 @@ export const placeOrderHandler = async (argv: Arguments<any>, side: OrderSide) =
subscription.on('end', () => {
if (noMatches) {
console.log('no matches found');
} else if (isMax) {
console.log('no more matches found');
} else if (remainingQuantity > 0) {
console.log(`no more matches found, ${satsToCoinsStr(remainingQuantity)} qty will be discarded`);
}
Expand Down
5 changes: 5 additions & 0 deletions lib/proto/xudrpc.swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions lib/proto/xudrpc_pb.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 30 additions & 1 deletion lib/proto/xudrpc_pb.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 50 additions & 4 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,19 +610,41 @@ class Service {
* If price is zero or unspecified a market order will get added.
*/
public placeOrder = async (
args: { pairId: string, price: number, quantity: number, orderId: string, side: number,
replaceOrderId: string, immediateOrCancel: boolean },
args: { pairId: string, price: number, quantity?: number, orderId: string, side: number,
replaceOrderId: string, immediateOrCancel: boolean, max?: boolean },
callback?: (e: ServicePlaceOrderEvent) => void,
) => {
argChecks.PRICE_NON_NEGATIVE(args);
argChecks.PRICE_MAX_DECIMAL_PLACES(args);
argChecks.HAS_PAIR_ID(args);
const { pairId, price, quantity, orderId, side, replaceOrderId, immediateOrCancel } = args;
const { pairId, price, quantity, orderId, side, replaceOrderId, immediateOrCancel, max } = args;

let calculatedQuantity: number;

if (max) {
if (side === OrderSide.Sell) {
const currency = pairId.split('/')[0];
calculatedQuantity = (await this.getBalance({ currency })).get(currency)?.channelBalance || 0;
} else {
const currency = pairId.split('/')[1];
const balance = (await this.getBalance({ currency })).get(currency)?.channelBalance || 0;

if (!price) {
calculatedQuantity = this.calculateBuyMaxMarketQuantity(pairId, balance);
} else {
calculatedQuantity = balance / price;
}
}

this.logger.debug(`max flag is true to place order, calculated quantity from balance is ${calculatedQuantity}`);
} else {
calculatedQuantity = quantity || 0;
}

const order: OwnMarketOrder | OwnLimitOrder = {
pairId,
price,
quantity,
quantity: calculatedQuantity,
isBuy: side === OrderSide.Buy,
localId: orderId || replaceOrderId,
};
Expand All @@ -648,6 +670,30 @@ class Service {
await this.orderBook.placeMarketOrder(placeOrderRequest);
}

private calculateBuyMaxMarketQuantity(pairId: string, balance: number) {
let result = 0;
let currentBalance = balance;

this.listOrders({ pairId, owner: Owner.Both, limit: 0, includeAliases: false }).forEach((orderArrays, _) => {
for (const order of orderArrays.sellArray) {
if (order.quantity && order.price) {
// market buy max calculation
const maxBuyableFromThisPrice = currentBalance / order.price;
const calculatedQuantity = (maxBuyableFromThisPrice > order.quantity) ? order.quantity : maxBuyableFromThisPrice;
result += calculatedQuantity;
currentBalance -= order.price * calculatedQuantity;

if (currentBalance === 0) {
// we filled our buy quantity with this order
break;
}
}
}
});

return result;
}

/** Removes a currency. */
public removeCurrency = async (args: { currency: string }) => {
argChecks.VALID_CURRENCY(args);
Expand Down
3 changes: 3 additions & 0 deletions proto/xudrpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,9 @@ message PlaceOrderRequest {
string replace_order_id = 6 [json_name = "replace_order_id"];
// Whether the order must be filled immediately and not allowed to enter the order book.
bool immediate_or_cancel = 7 [json_name = "immediate_or_cancel"];
// Whether to trade all available funds.
// If true, the quantity field is ignored.
bool max = 8 [json_name = "max"];
}
message PlaceOrderResponse {
// A list of own orders (or portions thereof) that matched the newly placed order.
Expand Down
61 changes: 61 additions & 0 deletions test/integration/Service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
import { OrderSide, Owner, SwapClientType } from '../../lib/constants/enums';
import p2pErrors from '../../lib/p2p/errors';
import Service from '../../lib/service/Service';
import Xud from '../../lib/Xud';
import { getTempDir } from '../utils';
import { ServiceOrderSidesArrays } from '../../lib/service/types';

chai.use(chaiAsPromised);

Expand Down Expand Up @@ -186,4 +188,63 @@ describe('API Service', () => {
});
await expect(shutdownPromise).to.be.fulfilled;
});

describe('Max Quantity Calculation', () => {
before(async () => {
const map = new Map<string, ServiceOrderSidesArrays>();
map.set('BTC/DAI', {
buyArray: [],
sellArray: [
{ quantity: 0.01, price: 20000, pairId: 'BTC/DAI', id: 'test_1', createdAt: 1, side: OrderSide.Sell,
isOwnOrder: false, nodeIdentifier: { nodePubKey: 'some_key' } },
{ quantity: 0.01, price: 50000, pairId: 'BTC/DAI', id: 'test_2', createdAt: 1, side: OrderSide.Sell,
isOwnOrder: false, nodeIdentifier: { nodePubKey: 'some_key2' } },
{ quantity: 0.05, price: 100000, pairId: 'BTC/DAI', id: 'test_2', createdAt: 1, side: OrderSide.Sell,
isOwnOrder: false, nodeIdentifier: { nodePubKey: 'some_key2' } },
],
});

sinon.createSandbox().stub(service, 'listOrders').returns(map);
});

it('should return `0` for 0 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0);
await expect(number).to.equal(0);
});

it('should return `0.005` for 100 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 100);
await expect(number).to.equal(0.005);
});

it('should return `0.01` for 200 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 200);
await expect(number).to.equal(0.01);
});

it('should return `0.016` for 500 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 500);
await expect(number).to.equal(0.016);
});

it('should return `0.02` for 700 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 700);
await expect(number).to.equal(0.02);
});

it('should return `0.021` for 800 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 800);
await expect(number).to.equal(0.021);
});

it('should return `0.07` for 5700 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 5700);
await expect(number).to.equal(0.07);
});

it('should return `0.07` for 10000 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 10000);
await expect(number).to.equal(0.07);
});
});
});
Loading