From 56e42dd6d9cba65ddf755857a08d1f73f1eba668 Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Thu, 25 Jun 2026 06:52:32 +0000 Subject: [PATCH] fix(sdk): compute detailed execution prices locally --- sdks/python/pmxt/client.py | 51 +++++++-------- .../test_execution_price_detailed_local.py | 31 +++++++++ sdks/typescript/pmxt/client.ts | 63 +++++++++---------- .../execution-price-detailed-local.test.ts | 37 +++++++++++ 4 files changed, 119 insertions(+), 63 deletions(-) create mode 100644 sdks/python/tests/test_execution_price_detailed_local.py create mode 100644 sdks/typescript/tests/execution-price-detailed-local.test.ts diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index 53044b6f..44039727 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -3397,38 +3397,31 @@ def get_execution_price_detailed( Returns: Detailed execution result """ - try: - # Convert order_book to dict for API call - bids = [{"price": b.price, "size": b.size} for b in order_book.bids] - asks = [{"price": a.price, "size": a.size} for a in order_book.asks] - ob_dict = {"bids": bids, "asks": asks, "timestamp": order_book.timestamp} + if amount <= 0: + raise ValueError("Amount must be greater than 0") - body = { - "args": [ob_dict, side, amount] - } + levels = [level for level in (order_book.asks if side == "buy" else order_book.bids) if level.size > 0] + levels.sort(key=lambda level: level.price, reverse=(side == "sell")) - creds = self._get_credentials_dict() - if creds: - body["credentials"] = creds + if not levels: + return ExecutionPriceResult(price=0, filled_amount=0, fully_filled=False) - url = f"{self._resolve_sidecar_host()}/api/{self.exchange_name}/getExecutionPriceDetailed" + remaining_amount = amount + total_cost = 0.0 + filled_amount = 0.0 + epsilon = 0.00000001 - headers = {"Content-Type": "application/json", "Accept": "application/json"} - headers.update(self._get_auth_headers()) - - response = self._fetch_with_retry( - lambda: self._api_client.call_api( - method="POST", - url=url, - body=body, - header_params=headers - ) - ) + for level in levels: + if remaining_amount <= epsilon: + break - response.read() - data_json = json.loads(response.data) + fill_size = min(remaining_amount, level.size) + total_cost += fill_size * level.price + filled_amount += fill_size + remaining_amount -= fill_size - data = self._handle_response(data_json) - return _convert_execution_result(data) - except Exception as e: - raise self._parse_api_exception(e) from None + return ExecutionPriceResult( + price=total_cost / filled_amount if filled_amount > epsilon else 0, + filled_amount=filled_amount, + fully_filled=remaining_amount <= epsilon, + ) diff --git a/sdks/python/tests/test_execution_price_detailed_local.py b/sdks/python/tests/test_execution_price_detailed_local.py new file mode 100644 index 00000000..3c3b6f3d --- /dev/null +++ b/sdks/python/tests/test_execution_price_detailed_local.py @@ -0,0 +1,31 @@ +from pmxt import Polymarket +from pmxt.models import OrderBook, OrderLevel + + +def test_get_execution_price_detailed_computes_locally_without_sidecar(): + client = Polymarket(auto_start_server=False) + result = client.get_execution_price_detailed( + OrderBook( + bids=[OrderLevel(price=0.41, size=5), OrderLevel(price=0.43, size=10)], + asks=[OrderLevel(price=0.52, size=4), OrderLevel(price=0.5, size=6)], + ), + "buy", + 8, + ) + + assert abs(result.price - 0.505) < 1e-12 + assert result.filled_amount == 8 + assert result.fully_filled is True + + +def test_get_execution_price_detailed_reports_partial_fill(): + client = Polymarket(auto_start_server=False) + result = client.get_execution_price_detailed( + OrderBook(bids=[OrderLevel(price=0.42, size=2)], asks=[]), + "sell", + 5, + ) + + assert abs(result.price - 0.42) < 1e-12 + assert result.filled_amount == 2 + assert result.fully_filled is False diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index 9e075bf5..263760a4 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -2710,54 +2710,49 @@ export abstract class Exchange { } /** - * Calculate detailed execution price information. - * Uses the sidecar server for calculation to ensure consistency. + * Calculate detailed execution price information by walking the order book locally. * * @param orderBook - The current order book * @param side - 'buy' or 'sell' * @param amount - The amount to execute * @returns Detailed execution result */ - async getExecutionPriceDetailed( + getExecutionPriceDetailed( orderBook: OrderBook, side: 'buy' | 'sell', amount: number - ): Promise { - await this.initPromise; - try { - const body: any = { - args: [orderBook, side, amount] - }; - const credentials = this.getCredentials(); - if (credentials) { - body.credentials = credentials; - } + ): ExecutionPriceResult { + if (amount <= 0) { + throw new Error('Amount must be greater than 0'); + } - const url = `${this.resolveBaseUrl()}/api/${this.exchangeName}/getExecutionPriceDetailed`; + const levels = (side === 'buy' ? orderBook.asks : orderBook.bids) + .filter((level) => level.size > 0) + .sort((a, b) => side === 'buy' ? a.price - b.price : b.price - a.price); - const response = await this.fetchWithRetry(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...this.config.headers - }, - body: JSON.stringify(body) - }); + if (levels.length === 0) { + return { price: 0, filledAmount: 0, fullyFilled: false }; + } - if (!response.ok) { - const body = await response.json().catch(() => ({})); - if (body.error && typeof body.error === "object") { - throw fromServerError(body.error); - } - throw new PmxtError(body.error?.message || response.statusText); - } + let remainingAmount = amount; + let totalCost = 0; + let filledAmount = 0; + const epsilon = 0.00000001; - const json = await response.json(); - return this.handleResponse(json); - } catch (error) { - if (error instanceof PmxtError) throw error; - throw new PmxtError(`Failed to get execution price: ${error}`); + for (const level of levels) { + if (remainingAmount <= epsilon) break; + + const fillSize = Math.min(remainingAmount, level.size); + totalCost += fillSize * level.price; + filledAmount += fillSize; + remainingAmount -= fillSize; } + + return { + price: filledAmount > epsilon ? totalCost / filledAmount : 0, + filledAmount, + fullyFilled: remainingAmount <= epsilon, + }; } // ---------------------------------------------------------------------------- diff --git a/sdks/typescript/tests/execution-price-detailed-local.test.ts b/sdks/typescript/tests/execution-price-detailed-local.test.ts new file mode 100644 index 00000000..423e46f8 --- /dev/null +++ b/sdks/typescript/tests/execution-price-detailed-local.test.ts @@ -0,0 +1,37 @@ +import { Polymarket } from '../pmxt/client'; + +describe('getExecutionPriceDetailed', () => { + it('computes detailed execution price locally without sidecar access', () => { + const client = new Polymarket({ autoStartServer: false }); + const result = client.getExecutionPriceDetailed({ + bids: [ + { price: 0.41, size: 5 }, + { price: 0.43, size: 10 }, + ], + asks: [ + { price: 0.52, size: 4 }, + { price: 0.5, size: 6 }, + ], + }, 'buy', 8); + + expect(result).toEqual({ + price: 0.505, + filledAmount: 8, + fullyFilled: true, + }); + }); + + it('reports partial fills instead of requiring a network call', () => { + const client = new Polymarket({ autoStartServer: false }); + const result = client.getExecutionPriceDetailed({ + bids: [{ price: 0.42, size: 2 }], + asks: [], + }, 'sell', 5); + + expect(result).toEqual({ + price: 0.42, + filledAmount: 2, + fullyFilled: false, + }); + }); +});