polly-gamba 1.0.18 → 1.0.20
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 +8 -6
- package/dist/index.js +21 -0
- package/dist/position-monitor.d.ts +1 -0
- package/dist/position-monitor.js +48 -5
- package/package.json +1 -1
- package/service.log +106 -0
- package/settings.json +12 -0
- package/src/claude-trader.ts +8 -6
- package/src/index.ts +22 -0
- package/src/position-monitor.ts +59 -5
package/dist/claude-trader.js
CHANGED
|
@@ -254,7 +254,7 @@ For EVERY market above: if price differs from fair probability by >8%, place a t
|
|
|
254
254
|
await this.redis.ltrim(`${prefix}:reviews`, 0, 9999);
|
|
255
255
|
const positionLines = positions.map(p => {
|
|
256
256
|
const gainPct = (p.gain_pct * 100).toFixed(1);
|
|
257
|
-
const stopLossWarning = p.gain_pct <= -0.35 ? ' ⚠️ HARD STOP
|
|
257
|
+
const stopLossWarning = p.gain_pct <= -0.35 ? ' ⚠️ HARD STOP (-35%+ loss — Rule 2/4 MUST apply)' : '';
|
|
258
258
|
return `### ${p.market_id} ${p.market_question}
|
|
259
259
|
- Outcome: ${p.outcome} | Entry: ${p.entry_price} | Now: ${p.current_price} | Gain: ${gainPct}%${stopLossWarning}
|
|
260
260
|
- Size: $${p.size_usdc} | Hours to expiry: ${p.hours_to_expiry.toFixed(1)}h
|
|
@@ -267,12 +267,14 @@ Review each open position. For each position: check whether the exit trigger con
|
|
|
267
267
|
|
|
268
268
|
${positionLines}
|
|
269
269
|
|
|
270
|
-
CLOSE RULES
|
|
271
|
-
1. Exit trigger condition has been met (e.g. specific price level hit, event occurred)
|
|
272
|
-
2. Position is down >35% AND the fundamental thesis has materially weakened or been disproven
|
|
273
|
-
3.
|
|
270
|
+
CLOSE RULES — apply ALL that match, no thesis override allowed:
|
|
271
|
+
1. Exit trigger condition has been met (e.g. specific price level hit, event occurred). Apply literally.
|
|
272
|
+
2. Position is down >35% AND the fundamental thesis has materially weakened or been disproven (market structure changed, key assumption invalidated).
|
|
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
|
+
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 and both are losing — close the one with the higher percentage loss to reduce duplicate exposure.
|
|
274
276
|
|
|
275
|
-
HOLD RULES: If
|
|
277
|
+
HOLD RULES: If NONE of the close rules apply and exit trigger NOT triggered, do nothing (no output needed).`;
|
|
276
278
|
const msg = JSON.stringify({
|
|
277
279
|
type: 'user',
|
|
278
280
|
message: { role: 'user', content: prompt }
|
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/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
|
@@ -71,3 +71,109 @@ npm warn deprecated prebuild-install@7.1.3: No longer maintained. Please contact
|
|
|
71
71
|
/Users/feral/.npm/_npx/fe561c72834ff84d/node_modules/.bin/polly-gamba: line 1: use strict: command not found
|
|
72
72
|
/Users/feral/.npm/_npx/fe561c72834ff84d/node_modules/.bin/polly-gamba: line 2: syntax error near unexpected token `('
|
|
73
73
|
/Users/feral/.npm/_npx/fe561c72834ff84d/node_modules/.bin/polly-gamba: line 2: `var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {'
|
|
74
|
+
/Users/feral/.npm/_npx/fe561c72834ff84d/node_modules/.bin/polly-gamba: line 1: use strict: command not found
|
|
75
|
+
/Users/feral/.npm/_npx/fe561c72834ff84d/node_modules/.bin/polly-gamba: line 2: syntax error near unexpected token `('
|
|
76
|
+
/Users/feral/.npm/_npx/fe561c72834ff84d/node_modules/.bin/polly-gamba: line 2: `var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {'
|
|
77
|
+
npm warn deprecated prebuild-install@7.1.3: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
|
78
|
+
[polly-gamba] Starting paper trading service
|
|
79
|
+
[polly-gamba] Claude cwd: /Users/feral/polly-gamba
|
|
80
|
+
[polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
|
|
81
|
+
[coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
|
|
82
|
+
[polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
|
|
83
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
84
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
85
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
86
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
87
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
88
|
+
[expiring] 0 expiring markets for review
|
|
89
|
+
[expiring] no expiring markets found this cycle
|
|
90
|
+
[coinbase-ws] Connected
|
|
91
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
92
|
+
[scan] 16 high-quality markets for autonomous review
|
|
93
|
+
[claude-trader:anthropic] ready
|
|
94
|
+
[claude-trader:ollama] ready
|
|
95
|
+
[claude-trader:expiring] ready
|
|
96
|
+
[position-monitor] checked=25 review_candidates=8 (moved>5% or <72h expiry)
|
|
97
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
98
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
99
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
100
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
101
|
+
[position-monitor] checked=25 review_candidates=8 (moved>5% or <72h expiry)
|
|
102
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
103
|
+
[expiring] 0 expiring markets for review
|
|
104
|
+
[expiring] no expiring markets found this cycle
|
|
105
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
106
|
+
[scan] 16 high-quality markets for autonomous review
|
|
107
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
108
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
109
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
110
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
111
|
+
[position-monitor] checked=25 review_candidates=7 (moved>5% or <72h expiry)
|
|
112
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
113
|
+
[expiring] 0 expiring markets for review
|
|
114
|
+
[expiring] no expiring markets found this cycle
|
|
115
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
116
|
+
[scan] 16 high-quality markets for autonomous review
|
|
117
|
+
[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
118
|
+
[gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
|
|
119
|
+
[position-monitor] checked=25 review_candidates=8 (moved>5% or <72h expiry)
|
|
120
|
+
[gamma] Loaded 496 markets (filtered from 500)
|
|
121
|
+
[scan] 16 high-quality markets for autonomous review
|
|
122
|
+
[expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
|
|
123
|
+
[expiring] 0 expiring markets for review
|
|
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
|
package/settings.json
ADDED
package/src/claude-trader.ts
CHANGED
|
@@ -304,7 +304,7 @@ For EVERY market above: if price differs from fair probability by >8%, place a t
|
|
|
304
304
|
|
|
305
305
|
const positionLines = positions.map(p => {
|
|
306
306
|
const gainPct = (p.gain_pct * 100).toFixed(1)
|
|
307
|
-
const stopLossWarning = p.gain_pct <= -0.35 ? ' ⚠️ HARD STOP
|
|
307
|
+
const stopLossWarning = p.gain_pct <= -0.35 ? ' ⚠️ HARD STOP (-35%+ loss — Rule 2/4 MUST apply)' : ''
|
|
308
308
|
return `### ${p.market_id} ${p.market_question}
|
|
309
309
|
- Outcome: ${p.outcome} | Entry: ${p.entry_price} | Now: ${p.current_price} | Gain: ${gainPct}%${stopLossWarning}
|
|
310
310
|
- Size: $${p.size_usdc} | Hours to expiry: ${p.hours_to_expiry.toFixed(1)}h
|
|
@@ -318,12 +318,14 @@ Review each open position. For each position: check whether the exit trigger con
|
|
|
318
318
|
|
|
319
319
|
${positionLines}
|
|
320
320
|
|
|
321
|
-
CLOSE RULES
|
|
322
|
-
1. Exit trigger condition has been met (e.g. specific price level hit, event occurred)
|
|
323
|
-
2. Position is down >35% AND the fundamental thesis has materially weakened or been disproven
|
|
324
|
-
3.
|
|
321
|
+
CLOSE RULES — apply ALL that match, no thesis override allowed:
|
|
322
|
+
1. Exit trigger condition has been met (e.g. specific price level hit, event occurred). Apply literally.
|
|
323
|
+
2. Position is down >35% AND the fundamental thesis has materially weakened or been disproven (market structure changed, key assumption invalidated).
|
|
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
|
+
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 and both are losing — close the one with the higher percentage loss to reduce duplicate exposure.
|
|
325
327
|
|
|
326
|
-
HOLD RULES: If
|
|
328
|
+
HOLD RULES: If NONE of the close rules apply and exit trigger NOT triggered, do nothing (no output needed).`
|
|
327
329
|
|
|
328
330
|
const msg = JSON.stringify({
|
|
329
331
|
type: 'user',
|
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/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}`)
|