Skip to content
Closed
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
270 changes: 49 additions & 221 deletions core/api-doc-config.generated.json

Large diffs are not rendered by default.

63 changes: 32 additions & 31 deletions core/src/exchanges/limitless/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,44 +200,45 @@ export async function paginateLimitlessMarkets(
): Promise<any[]> {
const PAGE_SIZE = 25;
const targetLimit = requestedLimit || PAGE_SIZE;
const MAX_PAGES = 20; // Safety limit to prevent infinite loops

// Over-fetch by 70% because ~30% of markets lack tokens and get filtered out
// This ensures enough raw markets survive the token filter
const OVER_FETCH_FACTOR = 1.7;
const rawTargetLimit = Math.ceil(targetLimit * OVER_FETCH_FACTOR);

if (targetLimit <= PAGE_SIZE) {
if (rawTargetLimit <= PAGE_SIZE) {
const response = await fetcher.getActiveMarkets({
limit: targetLimit,
limit: rawTargetLimit,
page: 1,
sortBy: sortBy,
});
return response.data || [];
}

// Fetch more pages than theoretically needed to account for filtering
// ~33% of markets lack tokens and get filtered out, so we over-fetch
// by 70% to ensure we get enough valid markets after filtering
const estimatedPages = Math.ceil(targetLimit / PAGE_SIZE);
const pagesWithBuffer = Math.min(Math.ceil(estimatedPages * 1.7), MAX_PAGES);

const pageNumbers: number[] = [];
for (let i = 1; i <= pagesWithBuffer; i++) {
pageNumbers.push(i);
// Sequential pagination using totalMarketsCount
const allMarkets: any[] = [];
let page = 1;
const MAX_PAGES = 50; // Safety limit

while (page <= MAX_PAGES && allMarkets.length < rawTargetLimit) {
const response = await fetcher.getActiveMarkets({
limit: PAGE_SIZE,
page: page,
sortBy: sortBy,
});

const markets = response.data || [];
if (markets.length === 0) break;

allMarkets.push(...markets);

// Use totalMarketsCount to check if we're done
const totalCount = response.totalMarketsCount;
if (totalCount && allMarkets.length >= totalCount) break;

page++;
}

const pages = await Promise.all(pageNumbers.map(async (page) => {
try {
const response = await fetcher.getActiveMarkets({
limit: PAGE_SIZE,
page: page,
sortBy: sortBy,
});
return response.data || [];
} catch (e) {
return [];
}
}));

const allMarkets = pages.flat();

// Don't slice here - let the caller handle limiting after filtering
// This ensures we return enough raw markets for the caller to filter

// Return raw markets - caller will filter tokens and apply actual limit
return allMarkets;
}
}
163 changes: 38 additions & 125 deletions sdks/python/pmxt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,11 +843,7 @@ def _hosted_cancel_validation_routes(self) -> Tuple[str, Optional[str]]:
return "cancel_polymarket", None
if self.exchange_name == "opinion":
return "cancel_opinion_polygon", "cancel_opinion_bsc_pull"
if self.exchange_name == "limitless":
return "cancel_limitless_polygon", "cancel_limitless_base_pull"
raise NotSupported(
"Hosted trading is only supported for Polymarket, Opinion, and Limitless."
)
raise NotSupported("Hosted trading is only supported for Polymarket and Opinion.")

@staticmethod
def _hosted_typed_data(payload: Dict[str, Any], key: str) -> Dict[str, Any]:
Expand Down Expand Up @@ -1193,7 +1189,7 @@ def has(self) -> Dict[str, Any]:

# Low-Level API Access

def _call_method(self, method_name: str, params: Any = None) -> Any:
def _call_method(self, method_name: str, params: Optional[Dict[str, Any]] = None) -> Any:
"""Call any exchange method on the server by name."""
try:
url = f"{self._resolve_sidecar_host()}/api/{self.exchange_name}/{method_name}"
Expand Down Expand Up @@ -1423,11 +1419,9 @@ def fetch_series(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedS
except ApiException as e:
raise self._parse_api_exception(e) from None

def fetch_market(self, params: Optional[Union[dict, str]] = None, **kwargs) -> UnifiedMarket:
def fetch_market(self, params: Optional[dict] = None, **kwargs) -> UnifiedMarket:
try:
args = []
if isinstance(params, str):
params = {"market_id": params}
if kwargs:
params = {**(params or {}), **kwargs}
if params is not None:
Expand Down Expand Up @@ -1491,7 +1485,7 @@ def fetch_event_metadata(self, event_ticker: str) -> Dict[str, Any]:
except ApiException as e:
raise self._parse_api_exception(e) from None

def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[float] = None, params: Optional[FetchOrderBookParams] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]:
def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[float] = None, params: Optional[dict] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]:
try:
args = []
if kwargs:
Expand Down Expand Up @@ -1543,8 +1537,6 @@ def fetch_order_books(self, outcome_ids: List[Union[str, "MarketOutcome"]]) -> D
raise self._parse_api_exception(e) from None

def cancel_order(self, order_id: str) -> Order:
if self.is_hosted:
return self._hosted_cancel_order(order_id)
try:
args = []
args.append(order_id)
Expand All @@ -1565,12 +1557,6 @@ def cancel_order(self, order_id: str) -> Order:
raise self._parse_api_exception(e) from None

def fetch_order(self, order_id: str) -> Order:
if self.is_hosted:
response = self._hosted_request(
"fetch_order",
path_params={"order_id": order_id},
)
return self._hosted_single(response, "order", order_from_v0)
try:
args = []
args.append(order_id)
Expand All @@ -1591,16 +1577,6 @@ def fetch_order(self, order_id: str) -> Order:
raise self._parse_api_exception(e) from None

def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]:
if self.is_hosted:
resolved_address = resolve_wallet_address(self, None)
params: Dict[str, Any] = {"address": resolved_address}
if market_id is not None:
params["market_id"] = market_id
response = self._hosted_request(
"fetch_open_orders",
params=params,
)
return self._hosted_collection(response, "orders", order_from_v0)
try:
args = []
if market_id is not None:
Expand All @@ -1622,13 +1598,6 @@ def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]:
raise self._parse_api_exception(e) from None

def fetch_my_trades(self, params: Optional[dict] = None, **kwargs) -> List[UserTrade]:
if self.is_hosted:
resolved_address = resolve_wallet_address(self, None)
response = self._hosted_request(
"fetch_my_trades",
path_params={"address": resolved_address},
)
return self._hosted_collection(response, "trades", user_trade_from_v0)
try:
args = []
if kwargs:
Expand All @@ -1652,12 +1621,6 @@ def fetch_my_trades(self, params: Optional[dict] = None, **kwargs) -> List[UserT
raise self._parse_api_exception(e) from None

def fetch_closed_orders(self, params: Optional[dict] = None, **kwargs) -> List[Order]:
if self.is_hosted:
from pmxt.errors import NotSupported
raise NotSupported(
"fetch_closed_orders is not supported in hosted mode; "
"use fetch_my_trades to see executed fills instead.",
)
try:
args = []
if kwargs:
Expand All @@ -1681,12 +1644,6 @@ def fetch_closed_orders(self, params: Optional[dict] = None, **kwargs) -> List[O
raise self._parse_api_exception(e) from None

def fetch_all_orders(self, params: Optional[dict] = None, **kwargs) -> List[Order]:
if self.is_hosted:
from pmxt.errors import NotSupported
raise NotSupported(
"fetch_all_orders is not supported in hosted mode; "
"use fetch_open_orders + fetch_my_trades to reconstruct order history.",
)
try:
args = []
if kwargs:
Expand All @@ -1710,13 +1667,6 @@ def fetch_all_orders(self, params: Optional[dict] = None, **kwargs) -> List[Orde
raise self._parse_api_exception(e) from None

def fetch_positions(self, address: Optional[str] = None) -> List[Position]:
if self.is_hosted:
resolved_address = resolve_wallet_address(self, address)
response = self._hosted_request(
"fetch_positions",
path_params={"address": resolved_address},
)
return self._hosted_collection(response, "positions", position_from_v0)
try:
args = []
if address is not None:
Expand All @@ -1738,13 +1688,6 @@ def fetch_positions(self, address: Optional[str] = None) -> List[Position]:
raise self._parse_api_exception(e) from None

def fetch_balance(self, address: Optional[str] = None) -> List[Balance]:
if self.is_hosted:
resolved_address = resolve_wallet_address(self, address)
response = self._hosted_request(
"fetch_balance",
path_params={"address": resolved_address},
)
return self._hosted_collection(response, "balances", balance_from_v0)
try:
args = []
if address is not None:
Expand All @@ -1767,8 +1710,10 @@ def fetch_balance(self, address: Optional[str] = None) -> List[Balance]:

def unwatch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, **_compat_kwargs) -> None:
try:
args = []
outcome_id = _compat_id(outcome_id, _compat_kwargs)
body: dict = {"args": [_resolve_outcome_id(outcome_id)]}
args.append(_resolve_outcome_id(outcome_id))
body: dict = {"args": args}
creds = self._get_credentials_dict()
if creds:
body["credentials"] = creds
Expand Down Expand Up @@ -2296,7 +2241,7 @@ def fetch_ohlcv(
params_dict[key] = value

args = [outcome_id, params_dict]
query = {"outcomeId": outcome_id, **params_dict}
query = {"id": outcome_id, **params_dict}
data = self._handle_response(
self._sidecar_read_request("fetchOHLCV", query, args)
)
Expand Down Expand Up @@ -2356,7 +2301,7 @@ def fetch_trades(
params_dict[key] = value

args = [outcome_id, params_dict]
query = {"outcomeId": outcome_id, **params_dict}
query = {"id": outcome_id, **params_dict}
data = self._handle_response(
self._sidecar_read_request("fetchTrades", query, args)
)
Expand Down Expand Up @@ -2905,45 +2850,6 @@ def watch_user_transactions(self, callback: Optional[Callable[[Dict[str, Any]],
except ApiException as e:
raise self._parse_api_exception(e) from None

def pre_warm_market(self, outcome_id: str) -> None:
"""
Pre-warm the SDK's internal caches for a market outcome.

Args:
outcome_id: The CLOB Token ID for the outcome

Returns:
None
"""
self._call_method("preWarmMarket", outcome_id)
return None

def get_event_by_id(self, id: str) -> Optional[UnifiedEvent]:
"""
Fetch a single Probable event by its numeric ID.

Args:
id: The numeric event ID

Returns:
The event, or None if not found
"""
data = self._call_method("getEventById", id)
return _convert_event(data) if data is not None else None

def get_event_by_slug(self, slug: str) -> Optional[UnifiedEvent]:
"""
Fetch a single Probable event by its URL slug.

Args:
slug: The event slug

Returns:
The event, or None if not found
"""
data = self._call_method("getEventBySlug", slug)
return _convert_event(data) if data is not None else None

# Trading Methods (require authentication)

def _discover_hosted_account(self) -> dict:
Expand Down Expand Up @@ -3426,31 +3332,38 @@ def get_execution_price_detailed(
Returns:
Detailed execution result
"""
if amount <= 0:
raise ValueError("Amount must be greater than 0")
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}

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"))
body = {
"args": [ob_dict, side, amount]
}

if not levels:
return ExecutionPriceResult(price=0, filled_amount=0, fully_filled=False)
creds = self._get_credentials_dict()
if creds:
body["credentials"] = creds

remaining_amount = amount
total_cost = 0.0
filled_amount = 0.0
epsilon = 0.00000001
url = f"{self._resolve_sidecar_host()}/api/{self.exchange_name}/getExecutionPriceDetailed"

for level in levels:
if remaining_amount <= epsilon:
break
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
)
)

fill_size = min(remaining_amount, level.size)
total_cost += fill_size * level.price
filled_amount += fill_size
remaining_amount -= fill_size
response.read()
data_json = json.loads(response.data)

return ExecutionPriceResult(
price=total_cost / filled_amount if filled_amount > epsilon else 0,
filled_amount=filled_amount,
fully_filled=remaining_amount <= epsilon,
)
data = self._handle_response(data_json)
return _convert_execution_result(data)
except Exception as e:
raise self._parse_api_exception(e) from None
2 changes: 1 addition & 1 deletion sdks/typescript/pmxt/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1747,7 +1747,7 @@ export abstract class Exchange {
}
}

async fetchMatchedMarkets(params?: FetchMatchedMarketClustersParams): Promise<any[]> {
async fetchMatchedMarkets(params?: any): Promise<any[]> {
await this.initPromise;
try {
const args: any[] = [];
Expand Down
Loading