Skip to content
Open
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 @@ -17,6 +17,7 @@ import {
} from '~/utils/Constants';
import { getDurationSegment } from '~/utils/functions/getSegment';
import type { OrderBookMode } from '~/utils/orderbook/OrderBookIFs';
import { getMarketExecutionPrice } from '~/utils/orderbook/orderBookExecution';
import type { PositionIF } from '~/utils/UserDataIFs';
import PositionSize from '../OrderInput/PositionSIze/PositionSize';
import SizeInput from '../OrderInput/SizeInput/SizeInput';
Expand All @@ -34,7 +35,7 @@ export default function MarketCloseModal({ close, position }: PropsIF) {
const { symbolInfo, symbol } = useTradeDataStore();

const { executeMarketOrder } = useMarketOrderService();
const { buys, sells } = useOrderBookStore();
const { buys, sells, lastBookUpdateAt } = useOrderBookStore();

const [isProcessingOrder, setIsProcessingOrder] = useState(false);

Expand Down Expand Up @@ -255,10 +256,39 @@ export default function MarketCloseModal({ close, position }: PropsIF) {
setIsProcessingOrder(true);

try {
// Get order book prices for the closing order
const closingSide = isPositionLong ? 'sell' : 'buy';
const bestBidPrice = buys.length > 0 ? buys[0].px : markPx;
const bestAskPrice = sells.length > 0 ? sells[0].px : markPx;
const { price: executionPrice, error: bookError } =
getMarketExecutionPrice(
closingSide,
buys,
sells,
lastBookUpdateAt,
);
if (bookError || executionPrice === undefined) {
setIsProcessingOrder(false);
notifications.add({
title: t(
bookError === 'no_asks'
? 'transactions.orderBookNoAsksTitle'
: bookError === 'no_bids'
? 'transactions.orderBookNoBidsTitle'
: 'transactions.orderBookStaleTitle',
),
message: t(
bookError === 'no_asks'
? 'transactions.orderBookNoAsksMessage'
: bookError === 'no_bids'
? 'transactions.orderBookNoBidsMessage'
: 'transactions.orderBookStaleMessage',
),
icon: 'error',
});
return;
}
const bestBidPrice =
closingSide === 'sell' ? executionPrice : undefined;
const bestAskPrice =
closingSide === 'buy' ? executionPrice : undefined;

const timeOfTxBuildStart = Date.now();
// Execute market order in opposite direction to close position
Expand Down
58 changes: 49 additions & 9 deletions packages/frontend/app/components/Trade/OrderInput/OrderInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
type NotificationStoreIF,
} from '~/stores/NotificationStore';
import { useOrderBookStore } from '~/stores/OrderBookStore';
import { getMarketExecutionPrice } from '~/utils/orderbook/orderBookExecution';
import { usePythPrice } from '~/stores/PythPriceStore';
import { useTradeDataStore, type marginModesT } from '~/stores/TradeDataStore';
import { useUnifiedMarginStore } from '~/stores/UnifiedMarginStore';
Expand Down Expand Up @@ -296,7 +297,8 @@ function OrderInput({

const [selectedDenom, setSelectedDenom] = useState<OrderBookMode>('usd');

const { buys, sells, midPrice, usualResolution } = useOrderBookStore();
const { buys, sells, midPrice, usualResolution, lastBookUpdateAt } =
useOrderBookStore();
const midPriceRef = useRef(midPrice);
midPriceRef.current = midPrice;
const { useMockLeverage, mockMinimumLeverage } = useDebugStore();
Expand Down Expand Up @@ -1419,8 +1421,26 @@ function OrderInput({

try {
setIsProcessingOrder(true);
// Get best ask price for buy order
const bestAskPrice = sells.length > 0 ? sells[0].px : markPx;
const { price: bestAskPrice, error: bookError } =
getMarketExecutionPrice('buy', buys, sells, lastBookUpdateAt);
if (bookError || bestAskPrice === undefined) {
setIsProcessingOrder(false);
confirmOrderModal.close();
notifications.add({
title: t(
bookError === 'no_asks'
? 'transactions.orderBookNoAsksTitle'
: 'transactions.orderBookStaleTitle',
),
message: t(
bookError === 'no_asks'
? 'transactions.orderBookNoAsksMessage'
: 'transactions.orderBookStaleMessage',
),
icon: 'error',
});
return;
}
const usdValueOfOrderStr = formatNum(
roundDownToHundredth(notionalQtyNum * (bestAskPrice || 1)),
2,
Expand Down Expand Up @@ -1566,8 +1586,9 @@ function OrderInput({
}
}, [
notionalQtyNum,
buys,
sells,
markPx,
lastBookUpdateAt,
activeOptions.skipOpenOrderConfirm,
symbol,
executeMarketOrder,
Expand Down Expand Up @@ -1599,15 +1620,33 @@ function OrderInput({
const slug = makeSlug(10);

try {
// Get best bid price for sell order
const bestBidPrice = buys.length > 0 ? buys[0].px : markPx;
setIsProcessingOrder(true);
const { price: bestBidPrice, error: bookError } =
getMarketExecutionPrice('sell', buys, sells, lastBookUpdateAt);
if (bookError || bestBidPrice === undefined) {
setIsProcessingOrder(false);
confirmOrderModal.close();
notifications.add({
title: t(
bookError === 'no_bids'
? 'transactions.orderBookNoBidsTitle'
: 'transactions.orderBookStaleTitle',
),
message: t(
bookError === 'no_bids'
? 'transactions.orderBookNoBidsMessage'
: 'transactions.orderBookStaleMessage',
),
icon: 'error',
});
return;
}
const usdValueOfOrderStr = formatNum(
Math.round(notionalQtyNum * (bestBidPrice || 1) * 100) / 100,
Math.round(notionalQtyNum * bestBidPrice * 100) / 100,
2,
true,
true,
);
setIsProcessingOrder(true);
if (activeOptions.skipOpenOrderConfirm) {
confirmOrderModal.close();
notifications.add({
Expand Down Expand Up @@ -1748,7 +1787,8 @@ function OrderInput({
}, [
notionalQtyNum,
buys,
markPx,
sells,
lastBookUpdateAt,
activeOptions.skipOpenOrderConfirm,
symbol,
executeMarketOrder,
Expand Down
5 changes: 4 additions & 1 deletion packages/frontend/app/routes/trade/orderbook/orderbook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -573,8 +573,11 @@ const OrderBook: React.FC<OrderBookProps> = ({
console.warn(
'No orderbook updates for 5s, reconnecting...',
);
setOrderBookState(TableState.LOADING);
setWsError(
'Order book data is stale. Waiting for live updates...',
);
forceReconnect();
lastMessageTimeRef.current = Date.now();
}
}, 6000);

Expand Down
14 changes: 14 additions & 0 deletions packages/frontend/app/services/MarketOrderLogManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ class MarketOrderLogManager {
return this.fetchData();
}

/**
* Fetch the current on-chain log page immediately before building a transaction.
* Avoids submitting orders with a stale cached page from the 30s poll interval.
*/
async getLogPageForTransaction(): Promise<number> {
await this.forceRefresh();
if (this.cachedLogPage === undefined) {
throw new Error(
'Cannot fetch market order log page. Try again in a moment.',
);
}
return this.cachedLogPage;
}

private start(): void {
if (this.isPolling || !this.connection) {
return;
Expand Down
13 changes: 4 additions & 9 deletions packages/frontend/app/services/cancelOrderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,9 @@ export class CancelOrderService {
? params.orderId
: BigInt(params.orderId);

// Get the cached market order log page to avoid RPC call
const cachedLogPage = marketOrderLogManager.getCachedLogPage();
if (cachedLogPage !== undefined) {
console.log(
' - Using cached marketOrderLogPage:',
cachedLogPage,
);
}
const marketOrderLogPage =
await marketOrderLogManager.getLogPageForTransaction();
console.log(' - marketOrderLogPage:', marketOrderLogPage);

const transaction = await buildCancelOrderTransaction(
this.connection,
Expand All @@ -75,7 +70,7 @@ export class CancelOrderService {
user: userPublicKey,
actor: sessionPublicKey,
rentPayer: rentPayer,
marketOrderLogPage: cachedLogPage,
marketOrderLogPage,
},
);

Expand Down
15 changes: 5 additions & 10 deletions packages/frontend/app/services/limitOrderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,9 @@ export class LimitOrderService {
);
}

// Get the cached market order log page to avoid RPC call
const cachedLogPage = marketOrderLogManager.getCachedLogPage();
if (cachedLogPage !== undefined) {
console.log(
' - Using cached marketOrderLogPage:',
cachedLogPage,
);
}
const marketOrderLogPage =
await marketOrderLogManager.getLogPageForTransaction();
console.log(' - marketOrderLogPage:', marketOrderLogPage);

// Build the appropriate transaction based on side
if (params.side === 'buy') {
Expand All @@ -121,7 +116,7 @@ export class LimitOrderService {
userSetImBps: userSetImBps,
includesFillAtMarket: true,
cancelOrderId: params.replaceOrderId,
marketOrderLogPage: cachedLogPage,
marketOrderLogPage,
reduceOnly: params.reduceOnly,
};

Expand All @@ -147,7 +142,7 @@ export class LimitOrderService {
userSetImBps: userSetImBps,
includesFillAtMarket: true,
cancelOrderId: params.replaceOrderId,
marketOrderLogPage: cachedLogPage,
marketOrderLogPage,
reduceOnly: params.reduceOnly,
};

Expand Down
17 changes: 6 additions & 11 deletions packages/frontend/app/services/marketOrderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,9 @@ export class MarketOrderService {
console.log(' - Calculated userSetImBps:', userSetImBps);
}

// Get the cached market order log page to avoid RPC call
const cachedLogPage = marketOrderLogManager.getCachedLogPage();
if (cachedLogPage !== undefined) {
console.log(
' - Using cached marketOrderLogPage:',
cachedLogPage,
);
}
const marketOrderLogPage =
await marketOrderLogManager.getLogPageForTransaction();
console.log(' - marketOrderLogPage:', marketOrderLogPage);

// Calculate fill prices based on order book
let fillPrice: bigint;
Expand Down Expand Up @@ -136,7 +131,7 @@ export class MarketOrderService {
// Build the appropriate transaction based on side
if (params.side === 'buy') {
console.log(' - Building market BUY order...');
console.log(' - Log page:', cachedLogPage);
console.log(' - Log page:', marketOrderLogPage);

const orderParams: any = {
marketId: marketId,
Expand All @@ -152,7 +147,7 @@ export class MarketOrderService {
keeper: sessionPublicKey,
userSetImBps: userSetImBps,
includesFillAtMarket: true,
marketOrderLogPage: cachedLogPage,
marketOrderLogPage,
reduceOnly: params.reduceOnly,
};

Expand Down Expand Up @@ -180,7 +175,7 @@ export class MarketOrderService {
keeper: sessionPublicKey,
userSetImBps: userSetImBps,
includesFillAtMarket: true, // Ensure fill at market is included
marketOrderLogPage: cachedLogPage,
marketOrderLogPage,
reduceOnly: params.reduceOnly,
};

Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/app/stores/OrderBookStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface OrderBookStore {
resolutionPair: OrderRowResolutionIF,
) => void;
midPrice: number | null;
lastBookUpdateAt: number | null;
setMidPrice: (midPrice: number) => void;
usualResolution: OrderRowResolutionIF | null;
setUsualResolution: (resolution: OrderRowResolutionIF) => void;
Expand All @@ -40,16 +41,19 @@ export const useOrderBookStore = create<OrderBookStore>()(
sells: OrderBookRowIF[],
setMid?: boolean,
) => {
const lastBookUpdateAt = Date.now();
if (setMid) {
set({
buys: buys,
sells: sells,
midPrice: (buys[0].px + sells[0].px) / 2,
lastBookUpdateAt,
});
} else {
set({
buys,
sells,
lastBookUpdateAt,
});
}
},
Expand All @@ -66,6 +70,7 @@ export const useOrderBookStore = create<OrderBookStore>()(
})),
resolutionPairs: {},
midPrice: null,
lastBookUpdateAt: null,
setMidPrice: (midPrice: number) => set({ midPrice }),
usualResolution: null,
setUsualResolution: (resolution: OrderRowResolutionIF) =>
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/app/utils/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ export const getTxLink = (signature?: string | null) => {
*/
export const MARKET_ORDER_PRICE_OFFSET_USD = 50;

/** Reject market orders if the order book has not updated within this window. */
export const ORDER_BOOK_MAX_AGE_MS = 10_000;

export const wsUrls = [
MARKET_WS_ENDPOINT,
'wss://pulse-api-mock.liquidity.tools/ws',
Expand Down
Loading
Loading