fix: correct win detection by mapping tokenId→outcome instead of array index
Problem: redeemer used array index to match balances to payoutFractions, but Data API token order ≠ on-chain outcome index. This caused false wins (shares on losing side counted as winning). Fix: - Store yesTokenId/noTokenId per conditionId in sniperExecutor - Match tokens by ID (yes=outcome0, no=outcome1) instead of array position - Only process positions tracked by sniper (skip MM/other positions) - Inject token lookup via setSniperConditionLookup to avoid circular imports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+36
-7
@@ -545,6 +545,10 @@ const _skippedLosses = new Set();
|
||||
// Callback invoked when a win is detected — receives conditionId
|
||||
let _onWinCallback = null;
|
||||
|
||||
// Function to look up conditionId → { asset, yesTokenId, noTokenId }
|
||||
// Injected from sniper entry point to avoid circular imports
|
||||
let _getConditionInfo = null;
|
||||
|
||||
/**
|
||||
* Register a callback to be called when a sniper win is detected.
|
||||
* Callback signature: (conditionId: string) => void
|
||||
@@ -553,6 +557,14 @@ export function onSniperWin(cb) {
|
||||
_onWinCallback = cb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a function to look up sniper condition info (token mapping).
|
||||
* Used to correctly map token balances to outcome indices.
|
||||
*/
|
||||
export function setSniperConditionLookup(fn) {
|
||||
_getConditionInfo = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redeem sniper positions via Gnosis Safe.
|
||||
* Only redeems WINNING positions — skip losses (they can be manually cleared).
|
||||
@@ -639,19 +651,36 @@ export async function redeemSniperPositions() {
|
||||
// Determine winning outcome index (the one with payoutFraction > 0)
|
||||
const winningOutcome = payoutFractions[0] > 0 ? 0 : payoutFractions[1] > 0 ? 1 : -1;
|
||||
|
||||
const label = conditionId.slice(0, 12) + '...';
|
||||
|
||||
// Map token balances to outcome indices using sniper's token mapping.
|
||||
// yesTokenId = outcome 0 (clobTokenIds[0]), noTokenId = outcome 1
|
||||
const sniperInfo = _getConditionInfo ? _getConditionInfo(conditionId) : null;
|
||||
|
||||
// Build outcome→balance mapping (keyed by outcome index, not array index)
|
||||
const outcomeBalances = [0, 0];
|
||||
if (sniperInfo) {
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (tokens[i].tokenId === sniperInfo.yesTokenId) outcomeBalances[0] = balances[i];
|
||||
else if (tokens[i].tokenId === sniperInfo.noTokenId) outcomeBalances[1] = balances[i];
|
||||
}
|
||||
} else {
|
||||
// No sniper mapping — skip (not a sniper position)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Win = we hold shares on the winning outcome side
|
||||
const isWin = winningOutcome >= 0 && balances[winningOutcome] > 0;
|
||||
const expectedUsdc = balances.reduce(
|
||||
const winShares = winningOutcome >= 0 ? outcomeBalances[winningOutcome] : 0;
|
||||
const isWin = winShares > 0;
|
||||
const expectedUsdc = outcomeBalances.reduce(
|
||||
(sum, shares, i) => sum + shares * (payoutFractions[i] ?? 0), 0
|
||||
);
|
||||
|
||||
const label = conditionId.slice(0, 12) + '...';
|
||||
|
||||
// SNIPER: only redeem WINNERS — cache losses to skip next time
|
||||
if (!isWin) {
|
||||
_skippedLosses.add(conditionId);
|
||||
if (config.dryRun) {
|
||||
logger.info(`SNIPER[SIM] skip loss: ${label} — ${totalShares.toFixed(3)} shares, outcome=${winningOutcome} (cached)`);
|
||||
logger.info(`SNIPER[SIM] skip loss: ${label} — outcome=${winningOutcome}, win_shares=0 (cached)`);
|
||||
} else {
|
||||
logger.info(`SNIPER redeemer: skip loss ${label} — outcome=${winningOutcome}, no shares on winner`);
|
||||
}
|
||||
@@ -662,11 +691,11 @@ export async function redeemSniperPositions() {
|
||||
if (_onWinCallback) _onWinCallback(conditionId);
|
||||
|
||||
if (config.dryRun) {
|
||||
logger.money(`SNIPER[SIM] redeem: ${label} — ${balances[winningOutcome].toFixed(3)} shares on outcome ${winningOutcome} → ~$${expectedUsdc.toFixed(2)} USDC (WIN)`);
|
||||
logger.money(`SNIPER[SIM] redeem: ${label} — ${winShares.toFixed(3)} shares on outcome ${winningOutcome} → ~$${expectedUsdc.toFixed(2)} USDC (WIN)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`SNIPER redeemer: ${label} resolved WIN — outcome ${winningOutcome}, ${balances[winningOutcome].toFixed(3)} shares → ~$${expectedUsdc.toFixed(2)} USDC`);
|
||||
logger.info(`SNIPER redeemer: ${label} resolved WIN — outcome ${winningOutcome}, ${winShares.toFixed(3)} shares → ~$${expectedUsdc.toFixed(2)} USDC`);
|
||||
|
||||
// Call redeemPositions through Safe — winners only
|
||||
const data = ctfIface.encodeFunctionData('redeemPositions', [
|
||||
|
||||
@@ -16,15 +16,20 @@ import { getTimeMultiplier } from './sniperSizing.js';
|
||||
// In-memory tracking of placed snipe orders
|
||||
const activeSnipes = [];
|
||||
|
||||
// conditionId → asset mapping (for win detection → pause linkage)
|
||||
const conditionAssetMap = new Map();
|
||||
// conditionId → { asset, yesTokenId, noTokenId } mapping
|
||||
// yesTokenId = outcome 0 (clobTokenIds[0]), noTokenId = outcome 1 (clobTokenIds[1])
|
||||
const conditionInfoMap = new Map();
|
||||
|
||||
export function getActiveSnipes() {
|
||||
return [...activeSnipes];
|
||||
}
|
||||
|
||||
export function getConditionAsset(conditionId) {
|
||||
return conditionAssetMap.get(conditionId) || null;
|
||||
return conditionInfoMap.get(conditionId)?.asset || null;
|
||||
}
|
||||
|
||||
export function getConditionInfo(conditionId) {
|
||||
return conditionInfoMap.get(conditionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,8 +61,13 @@ export async function executeSnipe(market) {
|
||||
const label = question.slice(0, 40);
|
||||
const sim = config.dryRun ? '[SIM] ' : '';
|
||||
|
||||
// Track conditionId → asset for win detection
|
||||
conditionAssetMap.set(conditionId, asset.toLowerCase());
|
||||
// Track conditionId → { asset, token IDs } for win detection
|
||||
// yesTokenId = outcome 0 (clobTokenIds[0]), noTokenId = outcome 1 (clobTokenIds[1])
|
||||
conditionInfoMap.set(conditionId, {
|
||||
asset: asset.toLowerCase(),
|
||||
yesTokenId,
|
||||
noTokenId,
|
||||
});
|
||||
|
||||
const sides = [
|
||||
{ name: 'UP', tokenId: yesTokenId },
|
||||
|
||||
+3
-2
@@ -17,8 +17,8 @@ import { initClient } from './services/client.js';
|
||||
import { getUsdcBalance } from './services/client.js';
|
||||
import { initDashboard, appendLog, updateStatus, isDashboardActive } from './ui/dashboard.js';
|
||||
import { startSniperDetector, stopSniperDetector } from './services/sniperDetector.js';
|
||||
import { executeSnipe, getActiveSnipes, getConditionAsset } from './services/sniperExecutor.js';
|
||||
import { redeemSniperPositions, onSniperWin } from './services/ctf.js';
|
||||
import { executeSnipe, getActiveSnipes, getConditionAsset, getConditionInfo } from './services/sniperExecutor.js';
|
||||
import { redeemSniperPositions, onSniperWin, setSniperConditionLookup } from './services/ctf.js';
|
||||
import { getSchedule, isAssetInSession, getNextSessionInfo } from './services/schedule.js';
|
||||
import { getTimeMultiplier } from './services/sniperSizing.js';
|
||||
|
||||
@@ -181,6 +181,7 @@ function tickPause(asset) {
|
||||
}
|
||||
|
||||
onSniperWin(handleWin);
|
||||
setSniperConditionLookup(getConditionInfo);
|
||||
|
||||
// ── Market handler ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
+4
-3
@@ -19,8 +19,8 @@ import config from './config/index.js';
|
||||
import logger from './utils/logger.js';
|
||||
import { initClient } from './services/client.js';
|
||||
import { startSniperDetector, stopSniperDetector } from './services/sniperDetector.js';
|
||||
import { executeSnipe, getConditionAsset } from './services/sniperExecutor.js';
|
||||
import { redeemSniperPositions, onSniperWin } from './services/ctf.js';
|
||||
import { executeSnipe, getConditionAsset, getConditionInfo } from './services/sniperExecutor.js';
|
||||
import { redeemSniperPositions, onSniperWin, setSniperConditionLookup } from './services/ctf.js';
|
||||
import { getSchedule, isAssetInSession, getNextSessionInfo } from './services/schedule.js';
|
||||
import { getTimeMultiplier } from './services/sniperSizing.js';
|
||||
|
||||
@@ -93,8 +93,9 @@ function tickPause(asset) {
|
||||
}
|
||||
}
|
||||
|
||||
// Register win callback
|
||||
// Register win callback and token lookup for correct outcome mapping
|
||||
onSniperWin(handleWin);
|
||||
setSniperConditionLookup(getConditionInfo);
|
||||
|
||||
// ── Log session schedule ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user