Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
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