polly-gamba 1.0.22 → 1.0.27
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 +27 -5
- package/dist/lib/paper-trade.d.ts +43 -0
- package/dist/lib/paper-trade.js +71 -0
- package/dist/mcp-server.js +21 -52
- package/dist/position-monitor.js +7 -1
- package/dist/test/stress.d.ts +1 -0
- package/dist/test/stress.js +404 -0
- package/package.json +4 -2
- package/src/claude-trader.ts +28 -5
- package/src/lib/paper-trade.ts +110 -0
- package/src/mcp-server.ts +20 -58
- package/src/position-monitor.ts +8 -1
- package/src/test/stress.ts +541 -0
- package/service.log +0 -291
package/dist/claude-trader.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ClaudeTrader = void 0;
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
9
10
|
const readline_1 = require("readline");
|
|
10
11
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
11
12
|
function findClaudeBin() {
|
|
@@ -29,6 +30,7 @@ function findClaudeBin() {
|
|
|
29
30
|
}
|
|
30
31
|
const CLAUDE_BIN = findClaudeBin();
|
|
31
32
|
const CLAUDE_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY || '';
|
|
33
|
+
const PAPER_MODE = process.env.PAPER_MODE === 'true' || process.env.PAPER_MODE === '1';
|
|
32
34
|
class ClaudeTrader {
|
|
33
35
|
proc = null;
|
|
34
36
|
redis;
|
|
@@ -66,6 +68,11 @@ class ClaudeTrader {
|
|
|
66
68
|
'--verbose',
|
|
67
69
|
'--dangerously-skip-permissions',
|
|
68
70
|
];
|
|
71
|
+
// Explicitly load MCP config — --print mode doesn't auto-discover settings.json
|
|
72
|
+
const settingsPath = (0, path_1.join)(this.config.cwd, 'settings.json');
|
|
73
|
+
if ((0, fs_1.existsSync)(settingsPath)) {
|
|
74
|
+
args.push('--mcp-config', settingsPath);
|
|
75
|
+
}
|
|
69
76
|
if (this.config.model) {
|
|
70
77
|
args.push('--model', this.config.model);
|
|
71
78
|
}
|
|
@@ -94,12 +101,12 @@ class ClaudeTrader {
|
|
|
94
101
|
type: 'user',
|
|
95
102
|
message: {
|
|
96
103
|
role: 'user',
|
|
97
|
-
content: `You are a Polymarket paper trader running a high-volume moneyball strategy. Your job is to place paper trades on EVERY market where you have ANY opinion on fair value — even slight.
|
|
98
|
-
|
|
104
|
+
content: `You are a Polymarket ${PAPER_MODE ? 'PAPER TRADING (no real money)' : 'paper'} trader running a high-volume moneyball strategy. Your job is to place paper trades on EVERY market where you have ANY opinion on fair value — even slight.
|
|
105
|
+
${PAPER_MODE ? '\n⚠️ PAPER MODE: This is a test environment. Be AGGRESSIVE — trade more, skip less. Lower your threshold to 3% edge for any market resolving within 6 months.\n' : ''}
|
|
99
106
|
TOOLS: place_order, skip_all, get_budget_status
|
|
100
107
|
RULES:
|
|
101
108
|
- Output ONLY tool calls. Zero prose.
|
|
102
|
-
- For EVERY market in the list: if current price differs from your estimated fair probability by more than 5
|
|
109
|
+
- For EVERY market in the list: if current price differs from your estimated fair probability by more than ${PAPER_MODE ? '3%' : '5%'}, place a trade.
|
|
103
110
|
- YES is underpriced → BUY YES. NO is underpriced (YES overpriced) → BUY NO.
|
|
104
111
|
- $10 USDC per trade (small size, many bets).
|
|
105
112
|
- NO cap on number of trades — bet every market where you see any edge.
|
|
@@ -178,7 +185,7 @@ Call place_order on any market you have edge on. $10 per trade. No cap.`;
|
|
|
178
185
|
const prefix = this.config.redisPrefix;
|
|
179
186
|
await this.redis.lpush(`${prefix}:scans`, JSON.stringify({ scanId, markets_count: markets.length, ts: Date.now() }));
|
|
180
187
|
await this.redis.ltrim(`${prefix}:scans`, 0, 9999);
|
|
181
|
-
const tradeable = markets.filter(m => m.tokens?.some(t => t.price
|
|
188
|
+
const tradeable = markets.filter(m => m.tokens?.some(t => t.price >= 0.10 && t.price <= 0.90));
|
|
182
189
|
const prompt = `## Autonomous Scan ${scanId}
|
|
183
190
|
|
|
184
191
|
Time: ${new Date().toISOString()}
|
|
@@ -192,7 +199,21 @@ ${tradeable.map((m, i) => `### ${i + 1}. ${m.question}
|
|
|
192
199
|
- Tokens: ${m.tokens?.map(t => `${t.outcome}@${t.price}`).join(', ') || 'none'}
|
|
193
200
|
${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('\n\n')}
|
|
194
201
|
|
|
195
|
-
|
|
202
|
+
## HORIZON DISCIPLINE (required — applied before edge check):
|
|
203
|
+
- Markets resolving WITHIN 6 months: ${PAPER_MODE ? '>3% edge required (PAPER MODE — be aggressive)' : 'standard >5% edge required'}.
|
|
204
|
+
- Markets resolving 6–12 months out: require >${PAPER_MODE ? '7' : '10'}% edge. Long-horizon bets tie up capital without near-term price catalysts.
|
|
205
|
+
- Markets resolving MORE than 12 months out: require >${PAPER_MODE ? '12' : '15'}% edge. Only trade if the mispricing is extreme and obvious.
|
|
206
|
+
- If the end date is unknown or ambiguous, treat it as >12 months. Do NOT guess short horizon.
|
|
207
|
+
- These thresholds stack: if the market already has an open position, do not add unless it meets the horizon threshold AND the 20%-discount re-entry rule.
|
|
208
|
+
|
|
209
|
+
## CONCENTRATION DISCIPLINE (required — applied before edge check):
|
|
210
|
+
- Count your existing open positions by underlying theme. A "theme" is the single real-world event that resolves the bet (e.g. "GTA VI release", "Hungary 2026 election", "NBA 2026 Finals").
|
|
211
|
+
- MAX 2 open positions per underlying theme. If you already have 2+ open positions in the same theme, SKIP all new markets in that theme regardless of apparent edge.
|
|
212
|
+
- Correlation examples: "Will Jesus return before GTA VI", "Will BTC hit $1M before GTA VI", "Will China invade Taiwan before GTA VI" — all three resolve based on GTA VI's release date. They are the SAME theme.
|
|
213
|
+
- Similarly: "Will Orbán be PM" and "Will Magyar be PM" are both the Hungary 2026 election — treat as same theme (you may hold both as a hedge pair, but do not add a third Hungary position).
|
|
214
|
+
- If you're uncertain whether a market correlates to an existing theme, assume it does and skip.
|
|
215
|
+
|
|
216
|
+
For EVERY market above: apply horizon discipline AND concentration discipline first, then if price differs from your fair probability by the required edge, place a trade ($10 USDC). Call skip_all only if you have zero opinion on all markets.`;
|
|
196
217
|
const msg = JSON.stringify({
|
|
197
218
|
type: 'user',
|
|
198
219
|
message: { role: 'user', content: prompt }
|
|
@@ -273,6 +294,7 @@ CLOSE RULES — apply ALL that match, no thesis override allowed:
|
|
|
273
294
|
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
295
|
4. Position is down >50% — HARD CLOSE regardless of thesis. Cut losses. No exceptions.
|
|
275
296
|
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.
|
|
297
|
+
6. Position is UP >25% from entry AND has been held >48h — CLOSE to lock in profits. The market has moved significantly in your favor; gains are now vulnerable to reversion. Use take_profit reason. Exception: if the resolution event is within 7 days AND the position is still clearly on the right side, you may hold until resolution.
|
|
276
298
|
|
|
277
299
|
HOLD RULES: If NONE of the close rules apply and exit trigger NOT triggered, do nothing (no output needed).`;
|
|
278
300
|
const msg = JSON.stringify({
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core business logic for paper trading.
|
|
3
|
+
* Extracted so it can be unit-tested independently of the MCP server.
|
|
4
|
+
*/
|
|
5
|
+
export interface Position {
|
|
6
|
+
market_id: string;
|
|
7
|
+
market_question: string;
|
|
8
|
+
outcome: string;
|
|
9
|
+
side: string;
|
|
10
|
+
size_usdc: number;
|
|
11
|
+
price: number;
|
|
12
|
+
ts: number;
|
|
13
|
+
status: 'open' | 'closed';
|
|
14
|
+
exit_price?: number;
|
|
15
|
+
pnl?: number;
|
|
16
|
+
closed_at?: number;
|
|
17
|
+
reason?: string;
|
|
18
|
+
exit_trigger?: string;
|
|
19
|
+
reasoning?: string;
|
|
20
|
+
new_catalyst?: string | null;
|
|
21
|
+
token_id?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface PlaceOrderParams {
|
|
24
|
+
market_id: string;
|
|
25
|
+
outcome: string;
|
|
26
|
+
size_usdc: number;
|
|
27
|
+
price: number;
|
|
28
|
+
new_catalyst?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const TOTAL_BUDGET = 500;
|
|
31
|
+
export declare const MARKET_CAP = 100;
|
|
32
|
+
/**
|
|
33
|
+
* Validate a place_order request against the current set of open positions.
|
|
34
|
+
* Returns an error message string if validation fails, or null if OK.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validatePlaceOrder(params: PlaceOrderParams, openPositions: Position[]): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Compute P&L for closing a position at a given exit price.
|
|
39
|
+
* Returns null if the entry price is zero or invalid (prevents division by zero).
|
|
40
|
+
*
|
|
41
|
+
* Formula: shares = size_usdc / entry_price; pnl = shares * exit_price - size_usdc
|
|
42
|
+
*/
|
|
43
|
+
export declare function computePnl(position: Position, exitPrice: number): number | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core business logic for paper trading.
|
|
4
|
+
* Extracted so it can be unit-tested independently of the MCP server.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MARKET_CAP = exports.TOTAL_BUDGET = void 0;
|
|
8
|
+
exports.validatePlaceOrder = validatePlaceOrder;
|
|
9
|
+
exports.computePnl = computePnl;
|
|
10
|
+
exports.TOTAL_BUDGET = 500;
|
|
11
|
+
exports.MARKET_CAP = 100;
|
|
12
|
+
/**
|
|
13
|
+
* Validate a place_order request against the current set of open positions.
|
|
14
|
+
* Returns an error message string if validation fails, or null if OK.
|
|
15
|
+
*/
|
|
16
|
+
function validatePlaceOrder(params, openPositions) {
|
|
17
|
+
// Input validation — guard against bad LLM output
|
|
18
|
+
if (params.size_usdc == null ||
|
|
19
|
+
typeof params.size_usdc !== 'number' ||
|
|
20
|
+
isNaN(params.size_usdc) ||
|
|
21
|
+
params.size_usdc <= 0) {
|
|
22
|
+
return `Error: size_usdc must be a positive number. Got: ${params.size_usdc}`;
|
|
23
|
+
}
|
|
24
|
+
if (params.price == null ||
|
|
25
|
+
typeof params.price !== 'number' ||
|
|
26
|
+
isNaN(params.price) ||
|
|
27
|
+
params.price <= 0 ||
|
|
28
|
+
params.price > 1) {
|
|
29
|
+
return `Error: price must be in range (0, 1]. Got: ${params.price}`;
|
|
30
|
+
}
|
|
31
|
+
const sameMarketPositions = openPositions.filter(p => p.market_id === params.market_id);
|
|
32
|
+
// Check 1: Existing position for same market requires new_catalyst
|
|
33
|
+
if (sameMarketPositions.length > 0 && !params.new_catalyst) {
|
|
34
|
+
return `Error: Position already exists for this market. Provide new_catalyst (specific new information from last 24h) to add.`;
|
|
35
|
+
}
|
|
36
|
+
// Check 1b: Same-outcome re-entry requires ≥20% price improvement
|
|
37
|
+
const sameOutcomePositions = sameMarketPositions.filter(p => String(p.outcome).toLowerCase() === String(params.outcome).toLowerCase());
|
|
38
|
+
if (sameOutcomePositions.length > 0) {
|
|
39
|
+
const lastEntry = Math.max(...sameOutcomePositions.map(p => p.price || 0));
|
|
40
|
+
const requiredPrice = lastEntry * 0.80;
|
|
41
|
+
if (params.price > requiredPrice) {
|
|
42
|
+
return `Error: Re-entry blocked. Last ${params.outcome} entry at ${lastEntry.toFixed(4)}. Require price ≤ ${requiredPrice.toFixed(4)} (20% better) to add same-direction position. Current price ${params.price.toFixed(4)} is too close to last entry. Wait for a larger dislocation before averaging.`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Check 2: $500 total budget cap
|
|
46
|
+
const totalDeployed = openPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
|
|
47
|
+
if (totalDeployed + params.size_usdc > exports.TOTAL_BUDGET) {
|
|
48
|
+
const available = Math.max(0, exports.TOTAL_BUDGET - totalDeployed);
|
|
49
|
+
return `Error: Budget cap reached. $${exports.TOTAL_BUDGET} paper budget. Currently deployed: $${totalDeployed.toFixed(2)}. Available: $${available.toFixed(2)}.`;
|
|
50
|
+
}
|
|
51
|
+
// Check 3: $100 per-market concentration cap
|
|
52
|
+
const marketDeployed = sameMarketPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
|
|
53
|
+
if (marketDeployed + params.size_usdc > exports.MARKET_CAP) {
|
|
54
|
+
return `Error: Single-market cap: max $${exports.MARKET_CAP} per market (20% of $${exports.TOTAL_BUDGET} budget). Already deployed $${marketDeployed.toFixed(2)} in this market.`;
|
|
55
|
+
}
|
|
56
|
+
return null; // valid
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compute P&L for closing a position at a given exit price.
|
|
60
|
+
* Returns null if the entry price is zero or invalid (prevents division by zero).
|
|
61
|
+
*
|
|
62
|
+
* Formula: shares = size_usdc / entry_price; pnl = shares * exit_price - size_usdc
|
|
63
|
+
*/
|
|
64
|
+
function computePnl(position, exitPrice) {
|
|
65
|
+
if (!position.price || position.price <= 0 || isNaN(position.price)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const shares = position.size_usdc / position.price;
|
|
69
|
+
return shares * exitPrice - position.size_usdc;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=paper-trade.js.map
|
package/dist/mcp-server.js
CHANGED
|
@@ -8,6 +8,7 @@ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
|
8
8
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
9
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
10
10
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
11
|
+
const paper_trade_1 = require("./lib/paper-trade");
|
|
11
12
|
const REDIS_PREFIX = process.env.POLLY_REDIS_PREFIX || 'polly';
|
|
12
13
|
const redis = new ioredis_1.default(process.env.REDIS_URL || 'redis://localhost:6379', {
|
|
13
14
|
retryStrategy: (times) => Math.min(times * 500, 5000),
|
|
@@ -110,8 +111,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
110
111
|
const ts = Date.now();
|
|
111
112
|
if (name === 'place_order') {
|
|
112
113
|
const a = args;
|
|
113
|
-
const TOTAL_BUDGET = 500;
|
|
114
|
-
const MARKET_CAP = 100;
|
|
115
114
|
// Load all open positions for enforcement checks
|
|
116
115
|
const rawPositions = await redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1);
|
|
117
116
|
const closedIds = new Set(await redis.smembers(`${REDIS_PREFIX}:closed_ids`));
|
|
@@ -127,50 +126,10 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
127
126
|
return false;
|
|
128
127
|
return !closedIds.has(`${p.market_id}_${p.outcome}_${p.ts}`);
|
|
129
128
|
});
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
return {
|
|
134
|
-
content: [{
|
|
135
|
-
type: 'text',
|
|
136
|
-
text: `Error: Position already exists for this market. Provide new_catalyst (specific new information from last 24h) to add.`
|
|
137
|
-
}]
|
|
138
|
-
};
|
|
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
|
-
}
|
|
154
|
-
// Check 2: $500 total budget cap
|
|
155
|
-
const totalDeployed = openPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
|
|
156
|
-
if (totalDeployed + a.size_usdc > TOTAL_BUDGET) {
|
|
157
|
-
const available = Math.max(0, TOTAL_BUDGET - totalDeployed);
|
|
158
|
-
return {
|
|
159
|
-
content: [{
|
|
160
|
-
type: 'text',
|
|
161
|
-
text: `Error: Budget cap reached. $500 paper budget. Currently deployed: $${totalDeployed.toFixed(2)}. Available: $${available.toFixed(2)}.`
|
|
162
|
-
}]
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
// Check 3: $100 per-market concentration cap
|
|
166
|
-
const marketDeployed = sameMarketPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
|
|
167
|
-
if (marketDeployed + a.size_usdc > MARKET_CAP) {
|
|
168
|
-
return {
|
|
169
|
-
content: [{
|
|
170
|
-
type: 'text',
|
|
171
|
-
text: `Error: Single-market cap: max $100 per market (20% of $500 budget). Already deployed $${marketDeployed.toFixed(2)} in this market.`
|
|
172
|
-
}]
|
|
173
|
-
};
|
|
129
|
+
// Validate using extracted business logic (includes input validation + budget checks)
|
|
130
|
+
const validationError = (0, paper_trade_1.validatePlaceOrder)({ market_id: a.market_id, outcome: a.outcome, size_usdc: a.size_usdc, price: a.price, new_catalyst: a.new_catalyst }, openPositions);
|
|
131
|
+
if (validationError) {
|
|
132
|
+
return { content: [{ type: 'text', text: validationError }] };
|
|
174
133
|
}
|
|
175
134
|
const position = {
|
|
176
135
|
market_id: a.market_id,
|
|
@@ -239,6 +198,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
239
198
|
if (name === 'close_position') {
|
|
240
199
|
const a = args;
|
|
241
200
|
const raw = await redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1);
|
|
201
|
+
const closedIdsForClose = new Set(await redis.smembers(`${REDIS_PREFIX}:closed_ids`));
|
|
242
202
|
const positions = raw.map(r => {
|
|
243
203
|
try {
|
|
244
204
|
return JSON.parse(r);
|
|
@@ -247,15 +207,24 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
247
207
|
return null;
|
|
248
208
|
}
|
|
249
209
|
}).filter(Boolean);
|
|
250
|
-
// Find matching open position
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
p.
|
|
210
|
+
// Find matching open position — must check BOTH status field AND closedIds set
|
|
211
|
+
// (hard stops add to closedIds but cannot update the list entry's status field)
|
|
212
|
+
const match = positions.find((p) => {
|
|
213
|
+
if (p.market_id !== a.market_id)
|
|
214
|
+
return false;
|
|
215
|
+
if (p.outcome?.toLowerCase() !== String(a.outcome).toLowerCase())
|
|
216
|
+
return false;
|
|
217
|
+
if (p.status === 'closed')
|
|
218
|
+
return false;
|
|
219
|
+
return !closedIdsForClose.has(`${p.market_id}_${p.outcome}_${p.ts}`);
|
|
220
|
+
});
|
|
254
221
|
if (!match) {
|
|
255
222
|
return { content: [{ type: 'text', text: `No open position found for market ${a.market_id} outcome ${a.outcome}` }] };
|
|
256
223
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
224
|
+
const pnl = (0, paper_trade_1.computePnl)(match, a.exit_price);
|
|
225
|
+
if (pnl === null) {
|
|
226
|
+
return { content: [{ type: 'text', text: `Error: Cannot compute P&L — entry price is zero or invalid for position ${a.market_id} ${a.outcome}` }] };
|
|
227
|
+
}
|
|
259
228
|
const closed = {
|
|
260
229
|
...match,
|
|
261
230
|
status: 'closed',
|
package/dist/position-monitor.js
CHANGED
|
@@ -65,8 +65,10 @@ class PositionMonitor {
|
|
|
65
65
|
const reviewCandidates = [];
|
|
66
66
|
for (const pos of toCheck) {
|
|
67
67
|
const currentPrice = marketPriceCache.get(`${pos.market_id}|${pos.outcome}`) ?? null;
|
|
68
|
-
if (!currentPrice)
|
|
68
|
+
if (!currentPrice) {
|
|
69
|
+
console.warn(`[position-monitor] WARNING: no price data for ${pos.market_id} ${pos.outcome} — skipping review (market may be resolved or delisted)`);
|
|
69
70
|
continue;
|
|
71
|
+
}
|
|
70
72
|
const entryPrice = pos.price;
|
|
71
73
|
const gain = (currentPrice.price - entryPrice) / entryPrice;
|
|
72
74
|
const hoursToEnd = (new Date(currentPrice.endDate).getTime() - Date.now()) / (1000 * 60 * 60);
|
|
@@ -127,6 +129,10 @@ class PositionMonitor {
|
|
|
127
129
|
const closedIds = await this.redis.smembers(`${prefix}:closed_ids`);
|
|
128
130
|
if (closedIds.includes(posKey))
|
|
129
131
|
return;
|
|
132
|
+
if (!match.price || match.price <= 0) {
|
|
133
|
+
console.error(`[position-monitor] HARD STOP skipped for "${String(match.market_question).slice(0, 50)}" — entry price is zero/invalid`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
130
136
|
const shares = match.size_usdc / match.price;
|
|
131
137
|
const pnl = shares * candidate.current_price - match.size_usdc;
|
|
132
138
|
const closed = { ...match, status: 'closed', exit_price: candidate.current_price, pnl, closed_at: Date.now(), reason: 'stop_loss' };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|