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

Emergency patch to handle invalid TimeInForce. #907

Merged
merged 5 commits into from
Dec 30, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ describe('protocolTranslations', () => {
['IOC', IndexerOrder_TimeInForce.TIME_IN_FORCE_IOC, TimeInForce.IOC],
['UNSPECIFIED', IndexerOrder_TimeInForce.TIME_IN_FORCE_UNSPECIFIED, TimeInForce.GTT],
['POST_ONLY', IndexerOrder_TimeInForce.TIME_IN_FORCE_POST_ONLY, TimeInForce.POST_ONLY],
// Test case for emergency patch
['INVALID', (100 as IndexerOrder_TimeInForce), TimeInForce.GTT],
])('successfully gets TimeInForce given protocol order TIF: %s', (
_name: string,
protocolTIF: IndexerOrder_TimeInForce,
Expand All @@ -176,13 +178,14 @@ describe('protocolTranslations', () => {
expect(protocolOrderTIFToTIF(protocolTIF)).toEqual(expectedTimeInForce);
});

it('throws error if unrecognized protocolTIF given', () => {
// Commented out for emergency patch
/* it('throws error if unrecognized protocolTIF given', () => {
expect(
() => {
protocolOrderTIFToTIF(100 as IndexerOrder_TimeInForce);
},
).toThrow(new Error('Unexpected TimeInForce from protocol: 100'));
});
});*/
});

describe('tifToProtocolOrderTIF', () => {
Expand Down
4 changes: 3 additions & 1 deletion indexer/packages/postgres/src/lib/protocol-translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ export function protocolOrderTIFToTIF(
protocolOrderTIF: IndexerOrder_TimeInForce,
): TimeInForce {
if (!(protocolOrderTIF in PROTOCOL_TIF_TO_INDEXER_TIF_MAP)) {
throw new Error(`Unexpected TimeInForce from protocol: ${protocolOrderTIF}`);
return TimeInForce.GTT;
// Removed for emergency patch
// throw new Error(`Unexpected TimeInForce from protocol: ${protocolOrderTIF}`);
}

return PROTOCOL_TIF_TO_INDEXER_TIF_MAP[protocolOrderTIF];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1289,12 +1289,236 @@ describe('OrderHandler', () => {
]);
});

// Testing emergency patch
it.each([
[
'via knex',
false,
],
[
'via SQL function',
true,
],
])('handles invalid TimeInForce (%s)', async (
_name: string,
useSqlFunction: boolean,
) => {
config.USE_ORDER_HANDLER_SQL_FUNCTION = useSqlFunction;
const transactionIndex: number = 0;
const eventIndex: number = 0;
const makerQuantums: number = 100;
const makerSubticks: number = 1_000_000;

const makerOrderProto: IndexerOrder = createOrder({
subaccountId: defaultSubaccountId,
clientId: 0,
side: IndexerOrder_Side.SIDE_BUY,
quantums: makerQuantums,
subticks: makerSubticks,
goodTilOneof: {
goodTilBlock: 10,
},
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
orderFlags: ORDER_FLAG_SHORT_TERM.toString(),
// Invalid TIF
timeInForce: (4 as IndexerOrder_TimeInForce),
reduceOnly: false,
clientMetadata: 0,
});

const takerSubticks: number = 150_000;
const takerQuantums: number = 10;
const takerOrderProto: IndexerOrder = createOrder({
subaccountId: defaultSubaccountId2,
clientId: 0,
side: IndexerOrder_Side.SIDE_SELL,
quantums: takerQuantums,
subticks: takerSubticks,
goodTilOneof: {
goodTilBlock: 10,
},
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
orderFlags: ORDER_FLAG_LONG_TERM.toString(),
// Invalid TIF
timeInForce: (17 as IndexerOrder_TimeInForce),
reduceOnly: true,
clientMetadata: 0,
});

// create initial PerpetualPositions with closed previous positions
await Promise.all([
// previous position for subaccount 1
PerpetualPositionTable.create({
...defaultPerpetualPosition,
perpetualId: testConstants.defaultPerpetualMarket3.id,
createdAtHeight: '1',
size: '0',
status: PerpetualPositionStatus.CLOSED,
openEventId: testConstants.defaultTendermintEventId2,
}),
// previous position for subaccount 2
PerpetualPositionTable.create({
...defaultPerpetualPosition,
perpetualId: testConstants.defaultPerpetualMarket3.id,
subaccountId: testConstants.defaultSubaccountId2,
createdAtHeight: '1',
size: '0',
status: PerpetualPositionStatus.CLOSED,
openEventId: testConstants.defaultTendermintEventId2,
}),
// initial position for subaccount 2
PerpetualPositionTable.create({
...defaultPerpetualPosition,
perpetualId: testConstants.defaultPerpetualMarket3.id,
}),
PerpetualPositionTable.create({
...defaultPerpetualPosition,
perpetualId: testConstants.defaultPerpetualMarket3.id,
subaccountId: testConstants.defaultSubaccountId2,
}),
]);

const fillAmount: number = 10;
const orderFillEvent: OrderFillEventV1 = createOrderFillEvent(
makerOrderProto,
takerOrderProto,
fillAmount,
fillAmount,
fillAmount,
);
const kafkaMessage: KafkaMessage = createKafkaMessageFromOrderFillEvent({
orderFillEvent,
transactionIndex,
eventIndex,
height: parseInt(defaultHeight, 10),
time: defaultTime,
txHash: defaultTxHash,
});

const producerSendMock: jest.SpyInstance = jest.spyOn(producer, 'send');
await onMessage(kafkaMessage);

// This price should be in fixed-point notation rather than exponential notation (1e-8)
const makerOrderSize: string = '1'; // quantums in human = 1e2 * 1e-2 = 1
const makerPrice: string = '0.00000000000001'; // quote currency / base currency = 1e6 * 1e-16 * 1e-6 / 1e-2 = 1e-14
const takerPrice: string = '0.0000000000000015'; // quote currency / base currency = 1.5e5 * 1e-16 * 1e-6 / 1e-2 = 1.5e-15
const totalFilled: string = '0.1'; // fillAmount in human = 1e1 * 1e-2 = 1e-1
await expectOrderInDatabase({
subaccountId: testConstants.defaultSubaccountId,
clientId: '0',
size: makerOrderSize,
totalFilled,
price: makerPrice,
status: OrderStatus.OPEN, // orderSize > totalFilled so status is open
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
side: protocolTranslations.protocolOrderSideToOrderSide(makerOrderProto.side),
orderFlags: makerOrderProto.orderId!.orderFlags.toString(),
// Invalid TIF in order is treated as GTT
timeInForce: TimeInForce.GTT,
reduceOnly: false,
goodTilBlock: protocolTranslations.getGoodTilBlock(makerOrderProto)?.toString(),
goodTilBlockTime: protocolTranslations.getGoodTilBlockTime(makerOrderProto),
clientMetadata: makerOrderProto.clientMetadata.toString(),
updatedAt: defaultDateTime.toISO(),
updatedAtHeight: defaultHeight.toString(),
});

const takerOrderSize: string = '0.1'; // quantums in human = 1e1 * 1e-2 = 1e-1
await expectOrderInDatabase({
subaccountId: testConstants.defaultSubaccountId2,
clientId: '0',
size: takerOrderSize,
totalFilled,
price: takerPrice,
status: OrderStatus.FILLED, // orderSize == totalFilled so status is filled
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
side: protocolTranslations.protocolOrderSideToOrderSide(takerOrderProto.side),
orderFlags: takerOrderProto.orderId!.orderFlags.toString(),
// Invalid TIF in order is treated as GTT
timeInForce: TimeInForce.GTT,
vincentwschau marked this conversation as resolved.
Show resolved Hide resolved
reduceOnly: true,
goodTilBlock: protocolTranslations.getGoodTilBlock(takerOrderProto)?.toString(),
goodTilBlockTime: protocolTranslations.getGoodTilBlockTime(takerOrderProto),
clientMetadata: takerOrderProto.clientMetadata.toString(),
updatedAt: defaultDateTime.toISO(),
updatedAtHeight: defaultHeight.toString(),
});

const eventId: Buffer = TendermintEventTable.createEventId(
defaultHeight,
transactionIndex,
eventIndex,
);

const quoteAmount: string = '0.000000000000001'; // quote amount is price * fillAmount = 1e-14 * 1e-1 = 1e-15
await expectFillInDatabase({
subaccountId: testConstants.defaultSubaccountId,
clientId: '0',
liquidity: Liquidity.MAKER,
size: totalFilled,
price: makerPrice,
quoteAmount,
eventId,
transactionHash: defaultTxHash,
createdAt: defaultDateTime.toISO(),
createdAtHeight: defaultHeight,
type: FillType.LIMIT,
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
side: protocolTranslations.protocolOrderSideToOrderSide(makerOrderProto.side),
orderFlags: makerOrderProto.orderId!.orderFlags.toString(),
clientMetadata: makerOrderProto.clientMetadata.toString(),
fee: defaultMakerFee,
});
await expectFillInDatabase({
subaccountId: testConstants.defaultSubaccountId2,
clientId: '0',
liquidity: Liquidity.TAKER,
size: totalFilled,
price: makerPrice,
quoteAmount,
eventId,
transactionHash: defaultTxHash,
createdAt: defaultDateTime.toISO(),
createdAtHeight: defaultHeight,
type: FillType.LIMIT,
clobPairId: testConstants.defaultPerpetualMarket3.clobPairId,
side: protocolTranslations.protocolOrderSideToOrderSide(takerOrderProto.side),
orderFlags: takerOrderProto.orderId!.orderFlags.toString(),
clientMetadata: takerOrderProto.clientMetadata.toString(),
fee: defaultTakerFee,
});

await Promise.all([
expectDefaultOrderAndFillSubaccountKafkaMessages(
producerSendMock,
eventId,
ORDER_FLAG_SHORT_TERM,
ORDER_FLAG_LONG_TERM,
testConstants.defaultPerpetualMarket3.id,
testConstants.defaultPerpetualMarket3.clobPairId,
),
expectDefaultTradeKafkaMessageFromTakerFillId(
producerSendMock,
eventId,
),
expectCandlesUpdated(),
expectStateFilledQuantums(
OrderTable.orderIdToUuid(makerOrderProto.orderId!),
orderFillEvent.totalFilledMaker.toString(),
),
expectStateFilledQuantums(
OrderTable.orderIdToUuid(takerOrderProto.orderId!),
orderFillEvent.totalFilledTaker.toString(),
),
]);
});

it.each([
[
undefined, // no maker order
],
[
IndexerOrder.fromPartial({ // no orderId
IndexerOrder.fromPartial({
orderId: undefined,
side: IndexerOrder_Side.SIDE_BUY,
quantums: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ BEGIN
WHEN '1'::jsonb THEN RETURN 'IOC';
WHEN '2'::jsonb THEN RETURN 'POST_ONLY';
WHEN '3'::jsonb THEN RETURN 'FOK';
ELSE RAISE EXCEPTION 'Unexpected TimeInForce from protocol %', tif;
ELSE RETURN 'GTT';
-- Removed for an emergency patch
-- ELSE RAISE EXCEPTION 'Unexpected TimeInForce from protocol %', tif;
END CASE;
END;
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;