Skip to content

Commit d1ba34c

Browse files
committed
feat: implement dynamic tick size and improve Limitless error handling
- Add tickSize support to UnifiedMarket and CreateOrderParams - Implement inferTickSize in Limitless and Polymarket exchanges - Improve error logging across Limitless exchange methods - Handle 404 in fetchPositions for new accounts - Add Limitless setup documentation and auth test script
1 parent 4fb3e9c commit d1ba34c

File tree

13 files changed

+178
-17
lines changed

13 files changed

+178
-17
lines changed

core/docs/SETUP_LIMITLESS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See [Polymarket setup](./SETUP_POLYMARKET.md), and use the ETH private key related to your Limitless account instead.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { config } from 'dotenv';
2+
import path from 'path';
3+
// Load .env from the root directory
4+
config({ path: path.resolve(__dirname, '../../../.env') });
5+
6+
import pmxt from '../../src/index';
7+
8+
async function testLimitless() {
9+
console.log('--- Initializing Limitless Client ---');
10+
if (!process.env.LIMITLESS_PRIVATE_KEY) {
11+
throw new Error('LIMITLESS_PRIVATE_KEY not found in .env');
12+
}
13+
14+
const client = new pmxt.Limitless({
15+
privateKey: process.env.LIMITLESS_PRIVATE_KEY
16+
});
17+
18+
try {
19+
console.log('\n--- Fetching Active Markets ---');
20+
const markets = await client.fetchMarkets({ limit: 5 });
21+
if (markets.length === 0) {
22+
console.log('No active markets found.');
23+
} else {
24+
console.table(markets.map(m => ({
25+
id: m.id,
26+
title: m.title.substring(0, 50),
27+
outcomes: m.outcomes.length,
28+
vol: m.volume24h
29+
})));
30+
}
31+
32+
console.log('\n--- Fetching Account Balance ---');
33+
const balance = await client.fetchBalance();
34+
console.table(balance);
35+
36+
console.log('\n--- Fetching Account Positions (REST API) ---');
37+
const positions = await client.fetchPositions();
38+
if (positions.length === 0) {
39+
const address = (client as any).ensureAuth().getAddress();
40+
console.log(`No open positions found for address: ${address}`);
41+
} else {
42+
console.table(positions);
43+
}
44+
45+
} catch (error: any) {
46+
console.error(`\nTest failed: ${error.message}`);
47+
}
48+
}
49+
50+
testLimitless();

core/src/exchanges/limitless/fetchMarkets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ export async function fetchMarkets(params?: MarketFilterParams): Promise<Unified
3939

4040
return unifiedMarkets.slice(offset, offset + limit);
4141

42-
} catch (error) {
43-
console.error("Error fetching Limitless data:", error);
42+
} catch (error: any) {
43+
console.error("Error fetching Limitless markets:", error.message);
4444
return [];
4545
}
4646
}

core/src/exchanges/limitless/fetchOHLCV.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export async function fetchOHLCV(id: string, params: HistoryFilterParams): Promi
3838
}).sort((a: any, b: any) => a.timestamp - b.timestamp);
3939

4040
} catch (error: any) {
41-
console.error(`Error fetching Limitless history for ${id}:`, error);
41+
console.error(`Error fetching Limitless history for ${id}:`, error.message);
4242
return [];
4343
}
4444
}

core/src/exchanges/limitless/fetchOrderBook.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export async function fetchOrderBook(id: string): Promise<OrderBook> {
3232
timestamp: Date.now() // API doesn't seem to return a specific timestamp in the root anymore
3333
};
3434

35-
} catch (error) {
36-
console.error(`Error fetching Limitless orderbook for ${id}:`, error);
35+
} catch (error: any) {
36+
console.error(`Error fetching Limitless orderbook for ${id}:`, error.message);
3737
return { bids: [], asks: [] };
3838
}
3939
}

core/src/exchanges/limitless/fetchPositions.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ export async function fetchPositions(userAddress: string): Promise<Position[]> {
1919
unrealizedPnL: parseFloat(p.cashPnl || '0'),
2020
realizedPnL: parseFloat(p.realizedPnl || '0')
2121
}));
22-
} catch (error) {
23-
console.error(`Error fetching Limitless positions for ${userAddress}:`, error);
22+
} catch (error: any) {
23+
// Limitless returns 404 if the user has no history on the platform.
24+
// We treat this as an empty portfolio rather than an error.
25+
if (error.response?.status === 404) {
26+
return [];
27+
}
28+
console.error(`Error fetching Limitless positions: ${error.message}`);
2429
return [];
2530
}
2631
}

core/src/exchanges/limitless/fetchTrades.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export async function fetchTrades(id: string, params: HistoryFilterParams): Prom
3030
}));
3131

3232
} catch (error: any) {
33-
console.error(`Error fetching Limitless trades for ${id}:`, error);
33+
console.error(`Error fetching Limitless trades for ${id}:`, error.message);
3434
return [];
3535
}
3636
}

core/src/exchanges/limitless/getMarketsBySlug.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export async function getMarketsBySlug(slug: string): Promise<UnifiedMarket[]> {
1616
const unifiedMarket = mapMarketToUnified(market);
1717
return unifiedMarket ? [unifiedMarket] : [];
1818

19-
} catch (error) {
20-
console.error(`Error fetching Limitless slug ${slug}:`, error);
19+
} catch (error: any) {
20+
console.error(`Error fetching Limitless slug ${slug}:`, error.message);
2121
return [];
2222
}
2323
}

core/src/exchanges/limitless/index.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export class LimitlessExchange extends PredictionMarketExchange {
3333
// New signature: LimitlessExchangeOptions
3434
credentials = options.credentials;
3535
wsConfig = options.websocket;
36+
} else if (options && 'privateKey' in options) {
37+
// Support direct privateKey for easier initialization
38+
credentials = options as ExchangeCredentials;
3639
} else {
3740
// Old signature: ExchangeCredentials directly
3841
credentials = options as ExchangeCredentials | undefined;
@@ -111,6 +114,21 @@ export class LimitlessExchange extends PredictionMarketExchange {
111114
// For market orders, use max slippage: 0.99 for BUY (willing to pay up to 99%), 0.01 for SELL (willing to accept down to 1%)
112115
const price = params.price || (side === Side.BUY ? 0.99 : 0.01);
113116

117+
// Auto-detect tick size if not provided
118+
let tickSize: string;
119+
if (params.tickSize) {
120+
tickSize = params.tickSize.toString();
121+
} else {
122+
// Fetch the order book to infer tick size from price levels
123+
try {
124+
const orderBook = await this.fetchOrderBook(params.outcomeId);
125+
tickSize = this.inferTickSize(orderBook);
126+
} catch (error) {
127+
// Fallback to 0.001 if order book fetch fails
128+
tickSize = "0.001";
129+
}
130+
}
131+
114132
try {
115133
// We use createAndPostOrder which handles signing and posting
116134
const response = await client.createAndPostOrder({
@@ -120,7 +138,7 @@ export class LimitlessExchange extends PredictionMarketExchange {
120138
size: params.amount,
121139
feeRateBps: 0,
122140
}, {
123-
tickSize: "0.01"
141+
tickSize: tickSize as any
124142
});
125143

126144
if (!response || !response.success) {
@@ -145,6 +163,41 @@ export class LimitlessExchange extends PredictionMarketExchange {
145163
}
146164
}
147165

166+
/**
167+
* Infer the tick size from order book price levels.
168+
* Analyzes the decimal precision of existing orders to determine the market's tick size.
169+
*/
170+
private inferTickSize(orderBook: OrderBook): string {
171+
const allPrices = [
172+
...orderBook.bids.map(b => b.price),
173+
...orderBook.asks.map(a => a.price)
174+
];
175+
176+
if (allPrices.length === 0) {
177+
return "0.001"; // Default fallback
178+
}
179+
180+
// Find the smallest non-zero decimal increment
181+
let minIncrement = 1;
182+
for (const price of allPrices) {
183+
const priceStr = price.toString();
184+
const decimalPart = priceStr.split('.')[1];
185+
if (decimalPart) {
186+
const decimals = decimalPart.length;
187+
const increment = Math.pow(10, -decimals);
188+
if (increment < minIncrement) {
189+
minIncrement = increment;
190+
}
191+
}
192+
}
193+
194+
// Map to valid tick sizes: 0.1, 0.01, 0.001, 0.0001
195+
if (minIncrement >= 0.1) return "0.1";
196+
if (minIncrement >= 0.01) return "0.01";
197+
if (minIncrement >= 0.001) return "0.001";
198+
return "0.0001";
199+
}
200+
148201
async cancelOrder(orderId: string): Promise<Order> {
149202
const auth = this.ensureAuth();
150203
const client = await auth.getClobClient();
@@ -216,7 +269,7 @@ export class LimitlessExchange extends PredictionMarketExchange {
216269
timestamp: o.created_at * 1000
217270
}));
218271
} catch (error: any) {
219-
console.error('Error fetching Limitless open orders:', error);
272+
console.error('Error fetching Limitless open orders:', error.message);
220273
return [];
221274
}
222275
}

core/src/exchanges/limitless/searchEvents.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export async function searchEvents(query: string, params?: MarketFilterParams):
3838
} as UnifiedEvent;
3939
});
4040

41-
} catch (error) {
42-
console.error("Error searching Limitless events:", error);
41+
} catch (error: any) {
42+
console.error("Error searching Limitless events:", error.message);
4343
return [];
4444
}
4545
}

0 commit comments

Comments
 (0)