polly-gamba 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ const GAMMA_BASE = 'https://gamma-api.polymarket.com';
2
+ const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
3
+
4
+ export interface MarketToken {
5
+ token_id: string;
6
+ outcome: string;
7
+ price: number;
8
+ }
9
+
10
+ export interface Market {
11
+ id: string;
12
+ conditionId: string;
13
+ question: string;
14
+ description: string;
15
+ volume: number;
16
+ volume24hr: number;
17
+ liquidity: number;
18
+ tokens: MarketToken[];
19
+ active: boolean;
20
+ closed: boolean;
21
+ }
22
+
23
+ interface CacheEntry {
24
+ markets: Market[];
25
+ ts: number;
26
+ }
27
+
28
+ let cache: CacheEntry | null = null;
29
+
30
+ function parseNumber(val: any): number {
31
+ if (typeof val === 'number') return val;
32
+ if (typeof val === 'string') return parseFloat(val) || 0;
33
+ return 0;
34
+ }
35
+
36
+ function parseTokens(raw: any): MarketToken[] {
37
+ if (!Array.isArray(raw)) return [];
38
+ return raw.map((t: any) => ({
39
+ token_id: t.token_id ?? t.tokenId ?? '',
40
+ outcome: t.outcome ?? '',
41
+ price: parseNumber(t.price),
42
+ }));
43
+ }
44
+
45
+ function parseMarket(raw: any): Market {
46
+ // Gamma API returns outcomes/prices as JSON strings, not a tokens array
47
+ let tokens = parseTokens(raw.tokens)
48
+ if (tokens.length === 0 && raw.outcomes) {
49
+ try {
50
+ const outcomes: string[] = JSON.parse(raw.outcomes)
51
+ const prices: string[] = JSON.parse(raw.outcomePrices || '[]')
52
+ tokens = outcomes.map((outcome, i) => ({
53
+ token_id: '',
54
+ outcome,
55
+ price: parseFloat(prices[i]) || 0,
56
+ }))
57
+ } catch {}
58
+ }
59
+
60
+ return {
61
+ id: String(raw.id ?? ''),
62
+ conditionId: String(raw.conditionId ?? raw.condition_id ?? ''),
63
+ question: String(raw.question ?? ''),
64
+ description: String(raw.description ?? ''),
65
+ volume: parseNumber(raw.volume),
66
+ volume24hr: parseNumber(raw.volume24hr),
67
+ liquidity: parseNumber(raw.liquidity),
68
+ tokens,
69
+ active: Boolean(raw.active),
70
+ closed: Boolean(raw.closed),
71
+ };
72
+ }
73
+
74
+ export async function fetchActiveMarkets(minVolume = 10_000, minLiquidity = 1_000): Promise<Market[]> {
75
+ const now = Date.now();
76
+ if (cache && now - cache.ts < CACHE_TTL_MS) {
77
+ return cache.markets;
78
+ }
79
+
80
+ const url = `${GAMMA_BASE}/markets?active=true&closed=false&limit=500`;
81
+ console.log(`[gamma] Fetching active markets from ${url}`);
82
+
83
+ const res = await fetch(url);
84
+ if (!res.ok) {
85
+ throw new Error(`Gamma API error: ${res.status} ${res.statusText}`);
86
+ }
87
+
88
+ const raw = await res.json();
89
+ const data: any[] = Array.isArray(raw) ? raw : [];
90
+ const markets: Market[] = data
91
+ .map(parseMarket)
92
+ .filter(m => m.active && !m.closed && m.volume >= minVolume && m.liquidity >= minLiquidity);
93
+
94
+ console.log(`[gamma] Loaded ${markets.length} markets (filtered from ${data.length})`);
95
+
96
+ cache = { markets, ts: now };
97
+ return markets;
98
+ }
99
+
100
+ export async function fetchHighQualityMarkets(
101
+ minVolume24h = 50_000,
102
+ minLiquidity = 50_000,
103
+ minPrice = 0.10,
104
+ maxPrice = 0.90
105
+ ): Promise<Market[]> {
106
+ const all = await fetchActiveMarkets(1_000, 1_000)
107
+
108
+ return all
109
+ .filter(m => {
110
+ const vol24 = m.volume24hr ?? 0
111
+ const liq = m.liquidity ?? 0
112
+ const hasUncertainty = m.tokens?.some(t => t.price >= minPrice && t.price <= maxPrice)
113
+ return vol24 >= minVolume24h && liq >= minLiquidity && hasUncertainty
114
+ })
115
+ .sort((a, b) => (b.volume24hr ?? 0) - (a.volume24hr ?? 0))
116
+ }
117
+
118
+ export async function fetchMarket(id: string): Promise<Market | null> {
119
+ const url = `${GAMMA_BASE}/markets/${id}`;
120
+ const res = await fetch(url);
121
+ if (!res.ok) return null;
122
+ const data = await res.json();
123
+ return parseMarket(data);
124
+ }
125
+
126
+ export function clearCache(): void {
127
+ cache = null;
128
+ }
129
+
130
+ const KEYWORDS: Record<string, string[]> = {
131
+ BTC: ['bitcoin', 'btc', 'crypto', 'cryptocurrency'],
132
+ ETH: ['ethereum', 'eth', 'ether', 'defi'],
133
+ }
134
+
135
+ export function filterBySignal(asset: string, markets: Market[]): Market[] {
136
+ const kw = KEYWORDS[asset] || []
137
+ const scored = markets
138
+ .map(m => {
139
+ const text = `${m.question} ${m.description || ''}`.toLowerCase()
140
+ const score = kw.reduce((s, k) => s + (text.includes(k) ? 1 : 0), 0)
141
+ return { m, score }
142
+ })
143
+ .filter(x => x.score > 0)
144
+ .sort((a, b) => b.score - a.score || b.m.volume - a.m.volume)
145
+ return scored.slice(0, 20).map(x => x.m)
146
+ }
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from '@modelcontextprotocol/sdk/types.js';
8
+
9
+ import { fetchActiveMarkets, fetchMarket, Market } from '../markets/gamma';
10
+ import { getOrderbook, getPrice, placeOrder } from '../markets/clob';
11
+ import { PriceSignal } from '../signals/coinbase-ws';
12
+
13
+ // Keywords that link crypto assets to market questions
14
+ const ASSET_KEYWORDS: Record<string, string[]> = {
15
+ BTC: ['bitcoin', 'btc', 'crypto', 'cryptocurrency', 'price', 'market'],
16
+ ETH: ['ethereum', 'eth', 'crypto', 'cryptocurrency', 'price', 'market'],
17
+ };
18
+
19
+ function matchesSignal(market: Market, signal: PriceSignal): boolean {
20
+ const keywords = ASSET_KEYWORDS[signal.asset] ?? [];
21
+ const haystack = `${market.question} ${market.description}`.toLowerCase();
22
+ return keywords.some(kw => haystack.includes(kw));
23
+ }
24
+
25
+ function simpleKeywordMatch(market: Market, query: string): boolean {
26
+ if (!query) return true;
27
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
28
+ const haystack = `${market.question} ${market.description}`.toLowerCase();
29
+ return terms.every(term => haystack.includes(term));
30
+ }
31
+
32
+ const server = new Server(
33
+ { name: 'polymarket', version: '1.0.0' },
34
+ { capabilities: { tools: {} } }
35
+ );
36
+
37
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
38
+ tools: [
39
+ {
40
+ name: 'list_markets',
41
+ description: 'Fetch active Polymarket prediction markets with current prices',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ query: { type: 'string', description: 'Keyword filter for market question/description' },
46
+ min_volume: { type: 'number', description: 'Minimum USD volume (default: 10000)' },
47
+ limit: { type: 'number', description: 'Max results to return (default: 20)' },
48
+ },
49
+ },
50
+ },
51
+ {
52
+ name: 'get_orderbook',
53
+ description: 'Get CLOB orderbook for a specific market token',
54
+ inputSchema: {
55
+ type: 'object',
56
+ required: ['token_id'],
57
+ properties: {
58
+ token_id: { type: 'string', description: 'Polymarket token ID' },
59
+ },
60
+ },
61
+ },
62
+ {
63
+ name: 'get_market_price',
64
+ description: 'Get current buy/sell price for a market token (0-1 = probability)',
65
+ inputSchema: {
66
+ type: 'object',
67
+ required: ['token_id', 'side'],
68
+ properties: {
69
+ token_id: { type: 'string' },
70
+ side: { type: 'string', enum: ['buy', 'sell'] },
71
+ },
72
+ },
73
+ },
74
+ {
75
+ name: 'assess_signal',
76
+ description: 'Given a crypto price signal, find relevant Polymarket markets that might be affected',
77
+ inputSchema: {
78
+ type: 'object',
79
+ required: ['signal'],
80
+ properties: {
81
+ signal: {
82
+ type: 'object',
83
+ description: 'PriceSignal object',
84
+ required: ['asset', 'direction', 'pct_change', 'price', 'window_secs', 'signal_ts'],
85
+ properties: {
86
+ asset: { type: 'string', enum: ['BTC', 'ETH'] },
87
+ direction: { type: 'string', enum: ['up', 'down'] },
88
+ pct_change: { type: 'number' },
89
+ price: { type: 'number' },
90
+ window_secs: { type: 'number' },
91
+ signal_ts: { type: 'number' },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ {
98
+ name: 'place_order',
99
+ description: 'Place a Polymarket CLOB order (dry_run=true by default — logs intent without executing)',
100
+ inputSchema: {
101
+ type: 'object',
102
+ required: ['token_id', 'side', 'size_usdc', 'price'],
103
+ properties: {
104
+ token_id: { type: 'string' },
105
+ side: { type: 'string', enum: ['BUY', 'SELL'] },
106
+ size_usdc: { type: 'number', description: 'Order size in USDC' },
107
+ price: { type: 'number', description: 'Limit price 0-1 (probability)' },
108
+ dry_run: { type: 'boolean', description: 'If true, log only — do not execute (default: true)' },
109
+ },
110
+ },
111
+ },
112
+ ],
113
+ }));
114
+
115
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
116
+ const { name, arguments: args } = request.params;
117
+
118
+ try {
119
+ switch (name) {
120
+ case 'list_markets': {
121
+ const { query, min_volume = 10_000, limit = 20 } = (args ?? {}) as any;
122
+ const markets = await fetchActiveMarkets(min_volume, 1_000);
123
+ const filtered = markets
124
+ .filter(m => simpleKeywordMatch(m, query ?? ''))
125
+ .slice(0, limit);
126
+ return {
127
+ content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }],
128
+ };
129
+ }
130
+
131
+ case 'get_orderbook': {
132
+ const { token_id } = args as { token_id: string };
133
+ const book = await getOrderbook(token_id);
134
+ return {
135
+ content: [{ type: 'text', text: JSON.stringify(book, null, 2) }],
136
+ };
137
+ }
138
+
139
+ case 'get_market_price': {
140
+ const { token_id, side } = args as { token_id: string; side: 'buy' | 'sell' };
141
+ const price = await getPrice(token_id, side);
142
+ return {
143
+ content: [{ type: 'text', text: JSON.stringify({ token_id, side, price }) }],
144
+ };
145
+ }
146
+
147
+ case 'assess_signal': {
148
+ const { signal } = args as { signal: PriceSignal };
149
+ const start = Date.now();
150
+ const markets = await fetchActiveMarkets(10_000, 1_000);
151
+ const matched = markets.filter(m => matchesSignal(m, signal));
152
+ const latency_ms = Date.now() - start;
153
+
154
+ const result = {
155
+ signal,
156
+ matched_count: matched.length,
157
+ latency_ms,
158
+ markets: matched.slice(0, 10).map(m => ({
159
+ id: m.id,
160
+ question: m.question,
161
+ volume: m.volume,
162
+ liquidity: m.liquidity,
163
+ tokens: m.tokens,
164
+ })),
165
+ };
166
+ return {
167
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
168
+ };
169
+ }
170
+
171
+ case 'place_order': {
172
+ const { token_id, side, size_usdc, price, dry_run = true } = args as any;
173
+ const result = await placeOrder({ token_id, side, size_usdc, price, dry_run });
174
+ return {
175
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
176
+ };
177
+ }
178
+
179
+ default:
180
+ throw new Error(`Unknown tool: ${name}`);
181
+ }
182
+ } catch (err: any) {
183
+ return {
184
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
185
+ isError: true,
186
+ };
187
+ }
188
+ });
189
+
190
+ async function main(): Promise<void> {
191
+ const transport = new StdioServerTransport();
192
+ await server.connect(transport);
193
+ console.error('[mcp] Polymarket MCP server started (stdio)');
194
+ }
195
+
196
+ main().catch((err) => {
197
+ console.error('[mcp] Fatal:', err);
198
+ process.exit(1);
199
+ });
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
5
+ import Redis from 'ioredis'
6
+
7
+ const REDIS_PREFIX = process.env.POLLY_REDIS_PREFIX || 'polly'
8
+
9
+ const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
10
+ retryStrategy: (times) => Math.min(times * 500, 5000),
11
+ maxRetriesPerRequest: null,
12
+ enableReadyCheck: false,
13
+ })
14
+ redis.on('error', (e) => console.error('[redis]', e.message))
15
+
16
+ const server = new Server(
17
+ { name: 'polly-gamba-paper', version: '1.0.0' },
18
+ { capabilities: { tools: {} } }
19
+ )
20
+
21
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
22
+ tools: [
23
+ {
24
+ name: 'place_order',
25
+ description: 'Place a paper trade on Polymarket. Logs the trade to Redis (no real execution).',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ market_id: { type: 'string', description: 'Polymarket market ID' },
30
+ market_question: { type: 'string', description: 'Market question for logging' },
31
+ token_id: { type: 'string', description: 'Token ID (YES or NO token)' },
32
+ outcome: { type: 'string', description: 'YES or NO' },
33
+ side: { type: 'string', enum: ['BUY', 'SELL'] },
34
+ size_usdc: { type: 'number', description: 'Size in USDC (virtual)' },
35
+ price: { type: 'number', description: 'Current price (0-1)' },
36
+ reasoning: { type: 'string', description: 'Why you are making this trade' }
37
+ },
38
+ required: ['market_id', 'market_question', 'outcome', 'side', 'size_usdc', 'price', 'reasoning']
39
+ }
40
+ },
41
+ {
42
+ name: 'skip_market',
43
+ description: 'Explicitly decide not to trade on a market, with reasoning.',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ market_id: { type: 'string' },
48
+ market_question: { type: 'string' },
49
+ reasoning: { type: 'string', description: 'Why you are skipping' }
50
+ },
51
+ required: ['market_id', 'market_question', 'reasoning']
52
+ }
53
+ },
54
+ {
55
+ name: 'skip_all',
56
+ description: 'Skip all markets in the current scan — use only when you have zero opinion on any market.',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ reasoning: { type: 'string', description: 'Why you are skipping all markets' }
61
+ },
62
+ required: ['reasoning']
63
+ }
64
+ },
65
+ {
66
+ name: 'get_positions',
67
+ description: 'Returns all open positions from Redis.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {},
71
+ required: []
72
+ }
73
+ },
74
+ {
75
+ name: 'close_position',
76
+ description: 'Marks a position as closed with exit price and P&L.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ market_id: { type: 'string', description: 'Polymarket market ID' },
81
+ outcome: { type: 'string', description: 'YES or NO' },
82
+ exit_price: { type: 'number', description: 'Exit price (0-1)' },
83
+ reason: { type: 'string', enum: ['take_profit', 'stop_loss', 'expiry', 'manual'], description: 'Reason for closing' }
84
+ },
85
+ required: ['market_id', 'outcome', 'exit_price', 'reason']
86
+ }
87
+ },
88
+ {
89
+ name: 'get_pnl_summary',
90
+ description: 'Returns P&L summary across all open and closed positions.',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {},
94
+ required: []
95
+ }
96
+ }
97
+ ]
98
+ }))
99
+
100
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
101
+ const { name, arguments: args } = req.params
102
+ const ts = Date.now()
103
+
104
+ if (name === 'place_order') {
105
+ const a = args as any
106
+ const position = { ...a, ts, status: 'open', pnl: null }
107
+ await redis.lpush(`${REDIS_PREFIX}:positions`, JSON.stringify(position))
108
+ await redis.ltrim(`${REDIS_PREFIX}:positions`, 0, 9999)
109
+ await redis.publish(`${REDIS_PREFIX}:events`, JSON.stringify({ type: 'trade', ...position }))
110
+ console.error(`[paper] TRADE ${a.side} ${a.outcome} "${String(a.market_question).slice(0, 50)}" @ ${a.price} $${a.size_usdc}`)
111
+ return {
112
+ content: [{
113
+ type: 'text',
114
+ text: `Paper trade logged: ${a.side} ${a.outcome} on "${a.market_question}" @ ${a.price} for $${a.size_usdc} USDC. Position recorded in Redis.`
115
+ }]
116
+ }
117
+ }
118
+
119
+ if (name === 'skip_market') {
120
+ const a = args as any
121
+ await redis.lpush(`${REDIS_PREFIX}:skips`, JSON.stringify({ ...a, ts }))
122
+ await redis.ltrim(`${REDIS_PREFIX}:skips`, 0, 9999)
123
+ console.error(`[paper] SKIP "${String(a.market_question).slice(0, 50)}"`)
124
+ return {
125
+ content: [{ type: 'text', text: `Skip recorded for "${a.market_question}": ${a.reasoning}` }]
126
+ }
127
+ }
128
+
129
+ if (name === 'skip_all') {
130
+ const a = args as any
131
+ await redis.lpush(`${REDIS_PREFIX}:skips`, JSON.stringify({ skip_all: true, reasoning: a.reasoning, ts }))
132
+ await redis.ltrim(`${REDIS_PREFIX}:skips`, 0, 9999)
133
+ console.error(`[paper] SKIP_ALL: ${String(a.reasoning).slice(0, 100)}`)
134
+ return {
135
+ content: [{ type: 'text', text: `Skip all recorded: ${a.reasoning}` }]
136
+ }
137
+ }
138
+
139
+ if (name === 'get_positions') {
140
+ const raw = await redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1)
141
+ const closedIds = new Set(await redis.smembers(`${REDIS_PREFIX}:closed_ids`))
142
+ const positions = raw.map(r => {
143
+ try { return JSON.parse(r) } catch { return null }
144
+ }).filter(Boolean).filter((p: any) => {
145
+ if (p.status === 'closed') return false
146
+ const key = `${p.market_id}_${p.outcome}_${p.ts}`
147
+ return !closedIds.has(key)
148
+ })
149
+ return {
150
+ content: [{ type: 'text', text: JSON.stringify(positions, null, 2) }]
151
+ }
152
+ }
153
+
154
+ if (name === 'close_position') {
155
+ const a = args as any
156
+ const raw = await redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1)
157
+ const positions = raw.map(r => {
158
+ try { return JSON.parse(r) } catch { return null }
159
+ }).filter(Boolean)
160
+
161
+ // Find matching open position
162
+ const match = positions.find((p: any) =>
163
+ p.market_id === a.market_id &&
164
+ p.outcome?.toLowerCase() === String(a.outcome).toLowerCase() &&
165
+ p.status !== 'closed'
166
+ )
167
+
168
+ if (!match) {
169
+ return { content: [{ type: 'text', text: `No open position found for market ${a.market_id} outcome ${a.outcome}` }] }
170
+ }
171
+
172
+ const shares = match.size_usdc / match.price
173
+ const pnl = shares * a.exit_price - match.size_usdc
174
+ const closed = {
175
+ ...match,
176
+ status: 'closed',
177
+ exit_price: a.exit_price,
178
+ pnl,
179
+ closed_at: Date.now(),
180
+ reason: a.reason,
181
+ }
182
+
183
+ const posKey = `${match.market_id}_${match.outcome}_${match.ts}`
184
+ await redis.sadd(`${REDIS_PREFIX}:closed_ids`, posKey)
185
+ await redis.lpush(`${REDIS_PREFIX}:closed_positions`, JSON.stringify(closed))
186
+ await redis.ltrim(`${REDIS_PREFIX}:closed_positions`, 0, 9999)
187
+ await redis.lpush(`${REDIS_PREFIX}:log`, JSON.stringify({ type: 'position_closed', data: closed, ts: Date.now() }))
188
+ await redis.ltrim(`${REDIS_PREFIX}:log`, 0, 9999)
189
+
190
+ console.error(`[paper] CLOSE ${a.reason.toUpperCase()} "${String(match.market_question).slice(0, 50)}" ${match.outcome} pnl=${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}`)
191
+ return {
192
+ content: [{ type: 'text', text: `Position closed: ${a.reason}. P&L: ${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)} USDC` }]
193
+ }
194
+ }
195
+
196
+ if (name === 'get_pnl_summary') {
197
+ const [openRaw, closedRaw] = await Promise.all([
198
+ redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1),
199
+ redis.lrange(`${REDIS_PREFIX}:closed_positions`, 0, -1),
200
+ ])
201
+ const closedIds = new Set(await redis.smembers(`${REDIS_PREFIX}:closed_ids`))
202
+
203
+ const allPositions = openRaw.map(r => { try { return JSON.parse(r) } catch { return null } }).filter(Boolean)
204
+ const openPositions = allPositions.filter((p: any) => {
205
+ if (p.status === 'closed') return false
206
+ return !closedIds.has(`${p.market_id}_${p.outcome}_${p.ts}`)
207
+ })
208
+ const closedPositions = closedRaw.map(r => { try { return JSON.parse(r) } catch { return null } }).filter(Boolean)
209
+
210
+ const totalInvested = openPositions.reduce((s: number, p: any) => s + (p.size_usdc || 0), 0)
211
+ const totalRealized = closedPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0)
212
+ const wins = closedPositions.filter((p: any) => (p.pnl || 0) > 0).length
213
+ const winRate = closedPositions.length > 0 ? wins / closedPositions.length : 0
214
+
215
+ const byReason: Record<string, number> = {}
216
+ for (const p of closedPositions) {
217
+ const r = p.reason || 'unknown'
218
+ byReason[r] = (byReason[r] || 0) + 1
219
+ }
220
+
221
+ const summary = {
222
+ open_positions: openPositions.length,
223
+ closed_positions: closedPositions.length,
224
+ total_invested: Math.round(totalInvested * 100) / 100,
225
+ total_realized_pnl: Math.round(totalRealized * 100) / 100,
226
+ win_rate: Math.round(winRate * 100) / 100,
227
+ by_reason: byReason,
228
+ }
229
+
230
+ return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] }
231
+ }
232
+
233
+ throw new Error(`Unknown tool: ${name}`)
234
+ })
235
+
236
+ async function main() {
237
+ const transport = new StdioServerTransport()
238
+ await server.connect(transport)
239
+ console.error('[mcp] polly-gamba-paper MCP server started (stdio)')
240
+ }
241
+
242
+ main().catch((e) => {
243
+ console.error('[mcp] Fatal:', e)
244
+ process.exit(1)
245
+ })