positions.check_exits
Automatically scans open positions and closes those meeting exit conditions: market resolution, stop-loss/take-profit triggers, or trader exit. Updates profit and loss accordingly.
Instructions
Scan all open positions for exit conditions: market resolution, stop-loss/take-profit triggers, or the original trader exiting. Updates P&L and closes positions that meet exit criteria. No parameters needed.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/index.ts:281-286 (registration)Tool registration for 'positions.check_exits' using server.tool() with no schema parameters, delegating to handleCheckExits.
server.tool( "positions.check_exits", "Scan all open positions for exit conditions: market resolution, stop-loss/take-profit triggers, or the original trader exiting. Updates P&L and closes positions that meet exit criteria. No parameters needed.", {}, safe("positions.check_exits", async () => ({ content: [{ type: "text" as const, text: await handleCheckExits(db) }] })) ); - src/tools/check-exits.ts:5-15 (handler)Handler function handleCheckExits that checks pro license and delegates to PositionTracker.checkExits().
export async function handleCheckExits(db: Database.Database): Promise<string> { const isPro = await checkLicense(); if (!isPro) return requirePro("check_exits"); const tracker = new PositionTracker(db); const closed = await tracker.checkExits(); if (closed === 0) { return "No positions resolved this cycle."; } return `${closed} position(s) resolved and P&L updated.`; } - src/services/position-tracker.ts:24-91 (handler)Core logic: PositionTracker.checkExits() iterates open positions, checking stop-loss/take-profit triggers, trader exits via Polymarket data API, and market resolution via CLOB API. Closes positions and updates P&L.
async checkExits(): Promise<number> { const openPositions = getOpenPositions(this.db); if (openPositions.length === 0) return 0; let closedCount = 0; for (const pos of openPositions) { try { // Guard: re-check position is still open (could have been closed by concurrent operation) const stillOpen = this.db.prepare( "SELECT 1 FROM trades WHERE id = ? AND status IN ('simulated', 'executed')" ).get(pos.id!); if (!stillOpen) continue; // Check 0: Stop-loss / take-profit if (pos.sl_price || pos.tp_price) { const currentPrice = await this.getCurrentPrice(pos.condition_id!); if (currentPrice > 0) { if (pos.sl_price && currentPrice <= pos.sl_price) { const pnl = this.calculatePnl(pos.price, currentPrice, pos.amount); updateTradeExit(this.db, pos.id!, currentPrice, "stop_loss", pnl); this.recycleBudget(pos.amount, pnl); log("trade", `Position closed (stop-loss): ${pos.market_slug} @ $${currentPrice.toFixed(2)} P&L: $${pnl.toFixed(2)}`); closedCount++; continue; } if (pos.tp_price && currentPrice >= pos.tp_price) { const pnl = this.calculatePnl(pos.price, currentPrice, pos.amount); updateTradeExit(this.db, pos.id!, currentPrice, "take_profit", pnl); this.recycleBudget(pos.amount, pnl); log("trade", `Position closed (take-profit): ${pos.market_slug} @ $${currentPrice.toFixed(2)} P&L: $${pnl.toFixed(2)}`); closedCount++; continue; } } } // Check 1: Did the trader exit? const traderExited = await this.checkTraderExit( pos.trader_address, pos.condition_id! ); if (traderExited) { const exitPrice = await this.getCurrentPrice(pos.condition_id!); const pnl = this.calculatePnl(pos.price, exitPrice, pos.amount); updateTradeExit(this.db, pos.id!, exitPrice, "trader_exit", pnl); this.recycleBudget(pos.amount, pnl); log("trade", `Position closed (trader exit): ${pos.market_slug} P&L: $${pnl.toFixed(2)}`); closedCount++; continue; } // Check 2: Did the market resolve? const resolution = await this.checkMarketResolved(pos.condition_id!, pos.token_id!); if (resolution !== null) { const pnl = this.calculatePnl(pos.price, resolution, pos.amount); updateTradeExit(this.db, pos.id!, resolution, "market_resolved", pnl); this.recycleBudget(pos.amount, pnl); log("trade", `Position resolved: ${pos.market_slug} → ${resolution === 1 ? "WIN" : "LOSS"} P&L: $${pnl.toFixed(2)}`); closedCount++; } } catch (err) { log("error", `Error tracking position ${pos.id}: ${err}`); } } return closedCount; } - checkTraderExit helper: queries Polymarket data API for SELL activity by the original trader to detect if they exited.
private async checkTraderExit(traderAddress: string, conditionId: string): Promise<boolean> { try { const url = `${DATA_API_BASE}/activity?user=${traderAddress}&type=TRADE&side=SELL&limit=20`; const res = await fetchWithRetry(url); if (!res.ok) return false; const activities = await res.json(); return activities.some((a: any) => a.conditionId === conditionId); } catch { return false; } } - checkMarketResolved helper: queries CLOB API to check if market is closed and determines if our token won or lost.
private async checkMarketResolved(conditionId: string, tokenId: string): Promise<number | null> { try { const url = `${CLOB_API_BASE}/markets/${conditionId}`; const res = await fetchWithRetry(url); if (!res.ok) return null; const market = await res.json(); if (!market.closed) return null; const tokens = market.tokens; if (!Array.isArray(tokens)) return null; // Find our token to check if it won const ourToken = tokens.find((t: any) => t.token_id === tokenId); if (!ourToken) { // Fallback: check any winner const winner = tokens.find((t: any) => t.winner === true); if (winner) return winner.price ?? 1.0; // No winners declared yet if (tokens.every((t: any) => t.winner === undefined || t.winner === null)) return null; return 0.0; } // Winner status not yet set — market closed but not resolved if (ourToken.winner === undefined || ourToken.winner === null) return null; return ourToken.winner ? 1.0 : 0.0; } catch { return null; } }