polly-gamba 1.0.19 → 1.0.22
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.
- package/dist/claude-trader.js +2 -2
- package/dist/index.js +21 -0
- package/dist/mcp-server.js +14 -0
- package/dist/position-monitor.d.ts +1 -0
- package/dist/position-monitor.js +48 -5
- package/package.json +1 -1
- package/service.log +167 -0
- package/settings.json +12 -0
- package/src/claude-trader.ts +2 -2
- package/src/index.ts +22 -0
- package/src/mcp-server.ts +17 -0
- package/src/position-monitor.ts +59 -5
package/dist/claude-trader.js
CHANGED
|
@@ -108,7 +108,7 @@ RULES:
|
|
|
108
108
|
|
|
109
109
|
## POSITION DISCIPLINE:
|
|
110
110
|
- Max $100 per market (20% of $500 budget). The MCP enforces this — don't fight it.
|
|
111
|
-
- To add to an existing position: you MUST cite a specific new catalyst (news published in last 24h, not price movement).
|
|
111
|
+
- To add to an existing position with the SAME outcome: (1) you MUST cite a specific new catalyst (news published in last 24h, not price movement), AND (2) the current price must be ≥20% below your last entry price (e.g. if you bought YES at 0.15, new entry only valid at ≤0.12). The MCP enforces this — attempts at a smaller discount will be rejected. Price dipping alone is NOT a catalyst. New information is a catalyst.
|
|
112
112
|
- exit_trigger is required on every trade. Be specific: "Exit when price hits 0.X" or "Exit when [specific news event]" — not "when narrative converges."
|
|
113
113
|
- Call get_budget_status at the start of each scan to know available capital.`
|
|
114
114
|
}
|
|
@@ -272,7 +272,7 @@ CLOSE RULES — apply ALL that match, no thesis override allowed:
|
|
|
272
272
|
2. Position is down >35% AND the fundamental thesis has materially weakened or been disproven (market structure changed, key assumption invalidated).
|
|
273
273
|
3. Your side of the position is priced below 10% — HARD CLOSE. The market has strongly repriced against you. Do NOT override this with "thesis not disproven yet." At <10%, expected value of holding is near zero.
|
|
274
274
|
4. Position is down >50% — HARD CLOSE regardless of thesis. Cut losses. No exceptions.
|
|
275
|
-
5. You have 2+ open positions in the SAME market
|
|
275
|
+
5. You have 2+ open positions in the SAME market with the SAME outcome — close the NEWER entry (higher ts value) regardless of P&L. Duplicate same-outcome positions double exposure without incremental edge. Keep the original entry.
|
|
276
276
|
|
|
277
277
|
HOLD RULES: If NONE of the close rules apply and exit trigger NOT triggered, do nothing (no output needed).`;
|
|
278
278
|
const msg = JSON.stringify({
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,26 @@ const os = __importStar(require("os"));
|
|
|
43
43
|
const fs_1 = require("fs");
|
|
44
44
|
const CWD = process.env.POLLY_CWD || path.join(os.homedir(), 'polly-gamba');
|
|
45
45
|
const EXPIRING_CWD = process.env.POLLY_EXPIRING_CWD || `${CWD}-expiring`;
|
|
46
|
+
function ensureMainCwdSettings(cwd) {
|
|
47
|
+
const settingsPath = path.join(cwd, 'settings.json');
|
|
48
|
+
if (!(0, fs_1.existsSync)(settingsPath)) {
|
|
49
|
+
const mcpServerPath = path.join(cwd, 'dist', 'mcp-server.js');
|
|
50
|
+
const settings = {
|
|
51
|
+
mcpServers: {
|
|
52
|
+
'polly-paper': {
|
|
53
|
+
command: 'node',
|
|
54
|
+
args: [mcpServerPath],
|
|
55
|
+
env: {
|
|
56
|
+
REDIS_URL: process.env.REDIS_URL || 'redis://localhost:6379',
|
|
57
|
+
POLLY_REDIS_PREFIX: 'polly',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
(0, fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2));
|
|
63
|
+
console.log(`[main] created settings.json at ${settingsPath}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
46
66
|
function ensureExpiringCwd(expiringCwd) {
|
|
47
67
|
if (!(0, fs_1.existsSync)(expiringCwd)) {
|
|
48
68
|
(0, fs_1.mkdirSync)(expiringCwd, { recursive: true });
|
|
@@ -69,6 +89,7 @@ function ensureExpiringCwd(expiringCwd) {
|
|
|
69
89
|
async function main() {
|
|
70
90
|
console.log('[polly-gamba] Starting paper trading service');
|
|
71
91
|
console.log(`[polly-gamba] Claude cwd: ${CWD}`);
|
|
92
|
+
ensureMainCwdSettings(CWD);
|
|
72
93
|
ensureExpiringCwd(EXPIRING_CWD);
|
|
73
94
|
console.log(`[polly-gamba] Expiring trader cwd: ${EXPIRING_CWD}`);
|
|
74
95
|
const anthropicTrader = new claude_trader_1.ClaudeTrader({ cwd: CWD, redisPrefix: 'polly', label: 'anthropic' });
|
package/dist/mcp-server.js
CHANGED
|
@@ -137,6 +137,20 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
137
137
|
}]
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
|
+
// Check 1b: Same-outcome re-entry requires ≥20% price improvement (prevents pyramiding into losers)
|
|
141
|
+
const sameOutcomePositions = sameMarketPositions.filter((p) => String(p.outcome).toLowerCase() === String(a.outcome).toLowerCase());
|
|
142
|
+
if (sameOutcomePositions.length > 0) {
|
|
143
|
+
const lastEntry = Math.max(...sameOutcomePositions.map((p) => p.price || 0));
|
|
144
|
+
const requiredPrice = lastEntry * 0.80;
|
|
145
|
+
if (a.price > requiredPrice) {
|
|
146
|
+
return {
|
|
147
|
+
content: [{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: `Error: Re-entry blocked. Last ${a.outcome} entry at ${lastEntry.toFixed(4)}. Require price ≤ ${requiredPrice.toFixed(4)} (20% better) to add same-direction position. Current price ${a.price.toFixed(4)} is too close to last entry. Wait for a larger dislocation before averaging.`
|
|
150
|
+
}]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
140
154
|
// Check 2: $500 total budget cap
|
|
141
155
|
const totalDeployed = openPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
|
|
142
156
|
if (totalDeployed + a.size_usdc > TOTAL_BUDGET) {
|
package/dist/position-monitor.js
CHANGED
|
@@ -87,15 +87,58 @@ class PositionMonitor {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// Execute programmatic hard stops BEFORE sending to Claude
|
|
91
|
+
// These are deterministic rules — no judgment needed
|
|
92
|
+
const hardStopClosed = [];
|
|
93
|
+
for (const candidate of reviewCandidates) {
|
|
94
|
+
const isPriceBelowFloor = candidate.current_price <= 0.10;
|
|
95
|
+
const isLargeEnoughLoss = candidate.gain_pct <= -0.50;
|
|
96
|
+
if (isPriceBelowFloor || isLargeEnoughLoss) {
|
|
97
|
+
await this.executeHardStop(candidate, isPriceBelowFloor ? 'price_below_floor' : 'stop_loss_50pct');
|
|
98
|
+
hardStopClosed.push(candidate.market_id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const remaining = reviewCandidates.filter(c => !hardStopClosed.includes(c.market_id));
|
|
102
|
+
console.log(`[position-monitor] checked=${toCheck.length} review_candidates=${reviewCandidates.length} hard_stops=${hardStopClosed.length} (moved>5% or <72h expiry)`);
|
|
103
|
+
// Pass remaining to Claude for judgment-based exits
|
|
104
|
+
if (remaining.length > 0 && this.trader) {
|
|
105
|
+
await this.trader.onPositionReview(remaining);
|
|
94
106
|
}
|
|
95
|
-
else if (
|
|
107
|
+
else if (remaining.length > 0) {
|
|
96
108
|
console.log('[position-monitor] no trader set — skipping Claude review');
|
|
97
109
|
}
|
|
98
110
|
}
|
|
111
|
+
async executeHardStop(candidate, reason) {
|
|
112
|
+
const prefix = this.prefix;
|
|
113
|
+
const raw = await this.redis.lrange(`${prefix}:positions`, 0, -1);
|
|
114
|
+
const positions = raw.map(r => { try {
|
|
115
|
+
return JSON.parse(r);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
} }).filter(Boolean);
|
|
120
|
+
const match = positions.find((p) => p.market_id === candidate.market_id &&
|
|
121
|
+
String(p.outcome).toLowerCase() === String(candidate.outcome).toLowerCase() &&
|
|
122
|
+
p.ts === candidate.ts &&
|
|
123
|
+
p.status !== 'closed');
|
|
124
|
+
if (!match)
|
|
125
|
+
return;
|
|
126
|
+
const posKey = `${match.market_id}_${match.outcome}_${match.ts}`;
|
|
127
|
+
const closedIds = await this.redis.smembers(`${prefix}:closed_ids`);
|
|
128
|
+
if (closedIds.includes(posKey))
|
|
129
|
+
return;
|
|
130
|
+
const shares = match.size_usdc / match.price;
|
|
131
|
+
const pnl = shares * candidate.current_price - match.size_usdc;
|
|
132
|
+
const closed = { ...match, status: 'closed', exit_price: candidate.current_price, pnl, closed_at: Date.now(), reason: 'stop_loss' };
|
|
133
|
+
await this.redis.sadd(`${prefix}:closed_ids`, posKey);
|
|
134
|
+
await this.redis.lpush(`${prefix}:closed_positions`, JSON.stringify(closed));
|
|
135
|
+
await this.redis.ltrim(`${prefix}:closed_positions`, 0, 9999);
|
|
136
|
+
await this.redis.lpush(`${prefix}:log`, JSON.stringify({ type: 'position_closed', data: closed, ts: Date.now(), trigger: reason }));
|
|
137
|
+
await this.redis.ltrim(`${prefix}:log`, 0, 9999);
|
|
138
|
+
const pnlStr = pnl >= 0 ? `+$${pnl.toFixed(2)}` : `-$${Math.abs(pnl).toFixed(2)}`;
|
|
139
|
+
const gainStr = (candidate.gain_pct * 100).toFixed(1);
|
|
140
|
+
console.log(`[position-monitor] HARD STOP [${reason}] "${String(match.market_question).slice(0, 50)}" ${match.outcome} entry=${match.price} exit=${candidate.current_price} gain=${gainStr}% pnl=${pnlStr}`);
|
|
141
|
+
}
|
|
99
142
|
async fetchCurrentPrice(marketId, outcome) {
|
|
100
143
|
try {
|
|
101
144
|
const res = await fetch(`https://gamma-api.polymarket.com/markets/${marketId}`);
|
package/package.json
CHANGED
package/service.log
CHANGED
|
@@ -122,3 +122,170 @@ npm warn deprecated prebuild-install@7.1.3: No longer maintained. Please contact
|
|
|
122
122
|
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
123
123
|
[expiring] 0 expiring markets for review
|
|
124
124
|
[expiring] no expiring markets found this cycle
|
|
125
|
+
[polly-gamba] Starting paper trading service
|
|
126
|
+
[polly-gamba] Claude cwd: /Users/feral/polly-gamba
|
|
127
|
+
[polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
|
|
128
|
+
[coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
|
|
129
|
+
[polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
|
|
130
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
131
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
132
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
133
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
134
|
+
[coinbase-ws] Connected
|
|
135
|
+
[position-monitor] checked=25 review_candidates=8 (moved>5% or <72h expiry)
|
|
136
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
137
|
+
[scan] 16 high-quality markets for autonomous review
|
|
138
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
139
|
+
[expiring] 0 expiring markets for review
|
|
140
|
+
[expiring] no expiring markets found this cycle
|
|
141
|
+
[claude-trader:anthropic] ready
|
|
142
|
+
[claude-trader:ollama] ready
|
|
143
|
+
[claude-trader:expiring] ready
|
|
144
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
145
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
146
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
147
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
148
|
+
[position-monitor] checked=25 review_candidates=8 (moved>5% or <72h expiry)
|
|
149
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
150
|
+
[scan] 16 high-quality markets for autonomous review
|
|
151
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
152
|
+
[expiring] 0 expiring markets for review
|
|
153
|
+
[expiring] no expiring markets found this cycle
|
|
154
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
155
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
156
|
+
[position-monitor] checked=25 review_candidates=8 (moved>5% or <72h expiry)
|
|
157
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
158
|
+
[scan] 16 high-quality markets for autonomous review
|
|
159
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
160
|
+
[expiring] 0 expiring markets for review
|
|
161
|
+
[expiring] no expiring markets found this cycle
|
|
162
|
+
[position-monitor] checked=25 review_candidates=8 (moved>5% or <72h expiry)
|
|
163
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
164
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
165
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
166
|
+
[scan] 16 high-quality markets for autonomous review
|
|
167
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
168
|
+
[expiring] 0 expiring markets for review
|
|
169
|
+
[expiring] no expiring markets found this cycle
|
|
170
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
171
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
172
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
173
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
174
|
+
[position-monitor] checked=24 review_candidates=7 (moved>5% or <72h expiry)
|
|
175
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
176
|
+
[scan] 16 high-quality markets for autonomous review
|
|
177
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
178
|
+
[expiring] 0 expiring markets for review
|
|
179
|
+
[expiring] no expiring markets found this cycle
|
|
180
|
+
[polly-gamba] Starting paper trading service
|
|
181
|
+
[polly-gamba] Claude cwd: /Users/feral/polly-gamba
|
|
182
|
+
[polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
|
|
183
|
+
[coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
|
|
184
|
+
[polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
|
|
185
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
186
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
187
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
188
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
189
|
+
[coinbase-ws] Connected
|
|
190
|
+
[position-monitor] checked=24 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
191
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
192
|
+
[expiring] 0 expiring markets for review
|
|
193
|
+
[expiring] no expiring markets found this cycle
|
|
194
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
195
|
+
[scan] 16 high-quality markets for autonomous review
|
|
196
|
+
[claude-trader:anthropic] ready
|
|
197
|
+
[claude-trader:ollama] ready
|
|
198
|
+
[claude-trader:expiring] ready
|
|
199
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
200
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
201
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
202
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
203
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
204
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
205
|
+
[expiring] 0 expiring markets for review
|
|
206
|
+
[expiring] no expiring markets found this cycle
|
|
207
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
208
|
+
[scan] 16 high-quality markets for autonomous review
|
|
209
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
210
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
211
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
212
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
213
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
214
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
215
|
+
[scan] 16 high-quality markets for autonomous review
|
|
216
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
217
|
+
[expiring] 0 expiring markets for review
|
|
218
|
+
[expiring] no expiring markets found this cycle
|
|
219
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
220
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
221
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
222
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
223
|
+
[scan] 16 high-quality markets for autonomous review
|
|
224
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
225
|
+
[expiring] 0 expiring markets for review
|
|
226
|
+
[expiring] no expiring markets found this cycle
|
|
227
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
228
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
229
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
230
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
231
|
+
[scan] 16 high-quality markets for autonomous review
|
|
232
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
233
|
+
[expiring] 0 expiring markets for review
|
|
234
|
+
[expiring] no expiring markets found this cycle
|
|
235
|
+
[polly-gamba] Starting paper trading service
|
|
236
|
+
[polly-gamba] Claude cwd: /Users/feral/polly-gamba
|
|
237
|
+
[polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
|
|
238
|
+
[coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
|
|
239
|
+
[polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
|
|
240
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
241
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
242
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
243
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
244
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
245
|
+
[expiring] 0 expiring markets for review
|
|
246
|
+
[expiring] no expiring markets found this cycle
|
|
247
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
248
|
+
[scan] 16 high-quality markets for autonomous review
|
|
249
|
+
[coinbase-ws] Connected
|
|
250
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
251
|
+
[claude-trader:anthropic] ready
|
|
252
|
+
[claude-trader:ollama] ready
|
|
253
|
+
[claude-trader:expiring] ready
|
|
254
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
255
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
256
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
257
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
258
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
259
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
260
|
+
[expiring] 0 expiring markets for review
|
|
261
|
+
[expiring] no expiring markets found this cycle
|
|
262
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
263
|
+
[scan] 16 high-quality markets for autonomous review
|
|
264
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
265
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
266
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
267
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
268
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
269
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
270
|
+
[scan] 16 high-quality markets for autonomous review
|
|
271
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
272
|
+
[expiring] 0 expiring markets for review
|
|
273
|
+
[expiring] no expiring markets found this cycle
|
|
274
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
275
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
276
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
277
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
278
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
279
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
280
|
+
[scan] 16 high-quality markets for autonomous review
|
|
281
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
282
|
+
[expiring] 0 expiring markets for review
|
|
283
|
+
[expiring] no expiring markets found this cycle
|
|
284
|
+
[position-monitor] checked=25 review_candidates=7 hard_stops=0 (moved>5% or <72h expiry)
|
|
285
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
286
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
287
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
288
|
+
[scan] 16 high-quality markets for autonomous review
|
|
289
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
290
|
+
[expiring] 0 expiring markets for review
|
|
291
|
+
[expiring] no expiring markets found this cycle
|
package/settings.json
ADDED
package/src/claude-trader.ts
CHANGED
|
@@ -124,7 +124,7 @@ RULES:
|
|
|
124
124
|
|
|
125
125
|
## POSITION DISCIPLINE:
|
|
126
126
|
- Max $100 per market (20% of $500 budget). The MCP enforces this — don't fight it.
|
|
127
|
-
- To add to an existing position: you MUST cite a specific new catalyst (news published in last 24h, not price movement).
|
|
127
|
+
- To add to an existing position with the SAME outcome: (1) you MUST cite a specific new catalyst (news published in last 24h, not price movement), AND (2) the current price must be ≥20% below your last entry price (e.g. if you bought YES at 0.15, new entry only valid at ≤0.12). The MCP enforces this — attempts at a smaller discount will be rejected. Price dipping alone is NOT a catalyst. New information is a catalyst.
|
|
128
128
|
- exit_trigger is required on every trade. Be specific: "Exit when price hits 0.X" or "Exit when [specific news event]" — not "when narrative converges."
|
|
129
129
|
- Call get_budget_status at the start of each scan to know available capital.`
|
|
130
130
|
}
|
|
@@ -323,7 +323,7 @@ CLOSE RULES — apply ALL that match, no thesis override allowed:
|
|
|
323
323
|
2. Position is down >35% AND the fundamental thesis has materially weakened or been disproven (market structure changed, key assumption invalidated).
|
|
324
324
|
3. Your side of the position is priced below 10% — HARD CLOSE. The market has strongly repriced against you. Do NOT override this with "thesis not disproven yet." At <10%, expected value of holding is near zero.
|
|
325
325
|
4. Position is down >50% — HARD CLOSE regardless of thesis. Cut losses. No exceptions.
|
|
326
|
-
5. You have 2+ open positions in the SAME market
|
|
326
|
+
5. You have 2+ open positions in the SAME market with the SAME outcome — close the NEWER entry (higher ts value) regardless of P&L. Duplicate same-outcome positions double exposure without incremental edge. Keep the original entry.
|
|
327
327
|
|
|
328
328
|
HOLD RULES: If NONE of the close rules apply and exit trigger NOT triggered, do nothing (no output needed).`
|
|
329
329
|
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,27 @@ import { mkdirSync, writeFileSync, existsSync } from 'fs'
|
|
|
9
9
|
const CWD = process.env.POLLY_CWD || path.join(os.homedir(), 'polly-gamba')
|
|
10
10
|
const EXPIRING_CWD = process.env.POLLY_EXPIRING_CWD || `${CWD}-expiring`
|
|
11
11
|
|
|
12
|
+
function ensureMainCwdSettings(cwd: string): void {
|
|
13
|
+
const settingsPath = path.join(cwd, 'settings.json')
|
|
14
|
+
if (!existsSync(settingsPath)) {
|
|
15
|
+
const mcpServerPath = path.join(cwd, 'dist', 'mcp-server.js')
|
|
16
|
+
const settings = {
|
|
17
|
+
mcpServers: {
|
|
18
|
+
'polly-paper': {
|
|
19
|
+
command: 'node',
|
|
20
|
+
args: [mcpServerPath],
|
|
21
|
+
env: {
|
|
22
|
+
REDIS_URL: process.env.REDIS_URL || 'redis://localhost:6379',
|
|
23
|
+
POLLY_REDIS_PREFIX: 'polly',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
|
29
|
+
console.log(`[main] created settings.json at ${settingsPath}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
function ensureExpiringCwd(expiringCwd: string): void {
|
|
13
34
|
if (!existsSync(expiringCwd)) {
|
|
14
35
|
mkdirSync(expiringCwd, { recursive: true })
|
|
@@ -37,6 +58,7 @@ async function main(): Promise<void> {
|
|
|
37
58
|
console.log('[polly-gamba] Starting paper trading service')
|
|
38
59
|
console.log(`[polly-gamba] Claude cwd: ${CWD}`)
|
|
39
60
|
|
|
61
|
+
ensureMainCwdSettings(CWD)
|
|
40
62
|
ensureExpiringCwd(EXPIRING_CWD)
|
|
41
63
|
console.log(`[polly-gamba] Expiring trader cwd: ${EXPIRING_CWD}`)
|
|
42
64
|
|
package/src/mcp-server.ts
CHANGED
|
@@ -138,6 +138,23 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// Check 1b: Same-outcome re-entry requires ≥20% price improvement (prevents pyramiding into losers)
|
|
142
|
+
const sameOutcomePositions = sameMarketPositions.filter((p: any) =>
|
|
143
|
+
String(p.outcome).toLowerCase() === String(a.outcome).toLowerCase()
|
|
144
|
+
)
|
|
145
|
+
if (sameOutcomePositions.length > 0) {
|
|
146
|
+
const lastEntry = Math.max(...sameOutcomePositions.map((p: any) => p.price || 0))
|
|
147
|
+
const requiredPrice = lastEntry * 0.80
|
|
148
|
+
if (a.price > requiredPrice) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: `Error: Re-entry blocked. Last ${a.outcome} entry at ${lastEntry.toFixed(4)}. Require price ≤ ${requiredPrice.toFixed(4)} (20% better) to add same-direction position. Current price ${a.price.toFixed(4)} is too close to last entry. Wait for a larger dislocation before averaging.`
|
|
153
|
+
}]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
// Check 2: $500 total budget cap
|
|
142
159
|
const totalDeployed = openPositions.reduce((s: number, p: any) => s + (p.size_usdc || 0), 0)
|
|
143
160
|
if (totalDeployed + a.size_usdc > TOTAL_BUDGET) {
|
package/src/position-monitor.ts
CHANGED
|
@@ -124,18 +124,72 @@ export class PositionMonitor {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// Execute programmatic hard stops BEFORE sending to Claude
|
|
128
|
+
// These are deterministic rules — no judgment needed
|
|
129
|
+
const hardStopClosed: string[] = []
|
|
130
|
+
for (const candidate of reviewCandidates) {
|
|
131
|
+
const isPriceBelowFloor = candidate.current_price <= 0.10
|
|
132
|
+
const isLargeEnoughLoss = candidate.gain_pct <= -0.50
|
|
133
|
+
if (isPriceBelowFloor || isLargeEnoughLoss) {
|
|
134
|
+
await this.executeHardStop(candidate, isPriceBelowFloor ? 'price_below_floor' : 'stop_loss_50pct')
|
|
135
|
+
hardStopClosed.push(candidate.market_id)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const remaining = reviewCandidates.filter(c => !hardStopClosed.includes(c.market_id))
|
|
140
|
+
|
|
127
141
|
console.log(
|
|
128
|
-
`[position-monitor] checked=${toCheck.length} review_candidates=${reviewCandidates.length} (moved>5% or <72h expiry)`
|
|
142
|
+
`[position-monitor] checked=${toCheck.length} review_candidates=${reviewCandidates.length} hard_stops=${hardStopClosed.length} (moved>5% or <72h expiry)`
|
|
129
143
|
)
|
|
130
144
|
|
|
131
|
-
// Pass to Claude for judgment-based exits
|
|
132
|
-
if (
|
|
133
|
-
await this.trader.onPositionReview(
|
|
134
|
-
} else if (
|
|
145
|
+
// Pass remaining to Claude for judgment-based exits
|
|
146
|
+
if (remaining.length > 0 && this.trader) {
|
|
147
|
+
await this.trader.onPositionReview(remaining)
|
|
148
|
+
} else if (remaining.length > 0) {
|
|
135
149
|
console.log('[position-monitor] no trader set — skipping Claude review')
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
|
|
153
|
+
private async executeHardStop(candidate: {
|
|
154
|
+
market_id: string
|
|
155
|
+
market_question: string
|
|
156
|
+
outcome: string
|
|
157
|
+
entry_price: number
|
|
158
|
+
current_price: number
|
|
159
|
+
gain_pct: number
|
|
160
|
+
size_usdc: number
|
|
161
|
+
ts: number
|
|
162
|
+
}, reason: string): Promise<void> {
|
|
163
|
+
const prefix = this.prefix
|
|
164
|
+
const raw = await this.redis.lrange(`${prefix}:positions`, 0, -1)
|
|
165
|
+
const positions = raw.map(r => { try { return JSON.parse(r) } catch { return null } }).filter(Boolean)
|
|
166
|
+
const match = positions.find((p: any) =>
|
|
167
|
+
p.market_id === candidate.market_id &&
|
|
168
|
+
String(p.outcome).toLowerCase() === String(candidate.outcome).toLowerCase() &&
|
|
169
|
+
p.ts === candidate.ts &&
|
|
170
|
+
p.status !== 'closed'
|
|
171
|
+
)
|
|
172
|
+
if (!match) return
|
|
173
|
+
|
|
174
|
+
const posKey = `${match.market_id}_${match.outcome}_${match.ts}`
|
|
175
|
+
const closedIds = await this.redis.smembers(`${prefix}:closed_ids`)
|
|
176
|
+
if (closedIds.includes(posKey)) return
|
|
177
|
+
|
|
178
|
+
const shares = match.size_usdc / match.price
|
|
179
|
+
const pnl = shares * candidate.current_price - match.size_usdc
|
|
180
|
+
const closed = { ...match, status: 'closed', exit_price: candidate.current_price, pnl, closed_at: Date.now(), reason: 'stop_loss' }
|
|
181
|
+
|
|
182
|
+
await this.redis.sadd(`${prefix}:closed_ids`, posKey)
|
|
183
|
+
await this.redis.lpush(`${prefix}:closed_positions`, JSON.stringify(closed))
|
|
184
|
+
await this.redis.ltrim(`${prefix}:closed_positions`, 0, 9999)
|
|
185
|
+
await this.redis.lpush(`${prefix}:log`, JSON.stringify({ type: 'position_closed', data: closed, ts: Date.now(), trigger: reason }))
|
|
186
|
+
await this.redis.ltrim(`${prefix}:log`, 0, 9999)
|
|
187
|
+
|
|
188
|
+
const pnlStr = pnl >= 0 ? `+$${pnl.toFixed(2)}` : `-$${Math.abs(pnl).toFixed(2)}`
|
|
189
|
+
const gainStr = (candidate.gain_pct * 100).toFixed(1)
|
|
190
|
+
console.log(`[position-monitor] HARD STOP [${reason}] "${String(match.market_question).slice(0, 50)}" ${match.outcome} entry=${match.price} exit=${candidate.current_price} gain=${gainStr}% pnl=${pnlStr}`)
|
|
191
|
+
}
|
|
192
|
+
|
|
139
193
|
private async fetchCurrentPrice(marketId: string, outcome: string): Promise<MarketPrice | null> {
|
|
140
194
|
try {
|
|
141
195
|
const res = await fetch(`https://gamma-api.polymarket.com/markets/${marketId}`)
|