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:
co-authored by
Claude Opus 4.6
parent
a8f28c961b
commit
d89b8ff54d
+36
-7
@@ -545,6 +545,10 @@ const _skippedLosses = new Set();
|
|||||||
// Callback invoked when a win is detected — receives conditionId
|
// Callback invoked when a win is detected — receives conditionId
|
||||||
let _onWinCallback = null;
|
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.
|
* Register a callback to be called when a sniper win is detected.
|
||||||
* Callback signature: (conditionId: string) => void
|
* Callback signature: (conditionId: string) => void
|
||||||
@@ -553,6 +557,14 @@ export function onSniperWin(cb) {
|
|||||||
_onWinCallback = 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.
|
* Redeem sniper positions via Gnosis Safe.
|
||||||
* Only redeems WINNING positions — skip losses (they can be manually cleared).
|
* 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)
|
// Determine winning outcome index (the one with payoutFraction > 0)
|
||||||
const winningOutcome = payoutFractions[0] > 0 ? 0 : payoutFractions[1] > 0 ? 1 : -1;
|
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
|
// Win = we hold shares on the winning outcome side
|
||||||
const isWin = winningOutcome >= 0 && balances[winningOutcome] > 0;
|
const winShares = winningOutcome >= 0 ? outcomeBalances[winningOutcome] : 0;
|
||||||
const expectedUsdc = balances.reduce(
|
const isWin = winShares > 0;
|
||||||
|
const expectedUsdc = outcomeBalances.reduce(
|
||||||
(sum, shares, i) => sum + shares * (payoutFractions[i] ?? 0), 0
|
(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
|
// SNIPER: only redeem WINNERS — cache losses to skip next time
|
||||||
if (!isWin) {
|
if (!isWin) {
|
||||||
_skippedLosses.add(conditionId);
|
_skippedLosses.add(conditionId);
|
||||||
if (config.dryRun) {
|
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 {
|
} else {
|
||||||
logger.info(`SNIPER redeemer: skip loss ${label} — outcome=${winningOutcome}, no shares on winner`);
|
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 (_onWinCallback) _onWinCallback(conditionId);
|
||||||
|
|
||||||
if (config.dryRun) {
|
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;
|
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
|
// Call redeemPositions through Safe — winners only
|
||||||
const data = ctfIface.encodeFunctionData('redeemPositions', [
|
const data = ctfIface.encodeFunctionData('redeemPositions', [
|
||||||
|
|||||||
@@ -16,15 +16,20 @@ import { getTimeMultiplier } from './sniperSizing.js';
|
|||||||
// In-memory tracking of placed snipe orders
|
// In-memory tracking of placed snipe orders
|
||||||
const activeSnipes = [];
|
const activeSnipes = [];
|
||||||
|
|
||||||
// conditionId → asset mapping (for win detection → pause linkage)
|
// conditionId → { asset, yesTokenId, noTokenId } mapping
|
||||||
const conditionAssetMap = new Map();
|
// yesTokenId = outcome 0 (clobTokenIds[0]), noTokenId = outcome 1 (clobTokenIds[1])
|
||||||
|
const conditionInfoMap = new Map();
|
||||||
|
|
||||||
export function getActiveSnipes() {
|
export function getActiveSnipes() {
|
||||||
return [...activeSnipes];
|
return [...activeSnipes];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConditionAsset(conditionId) {
|
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 label = question.slice(0, 40);
|
||||||
const sim = config.dryRun ? '[SIM] ' : '';
|
const sim = config.dryRun ? '[SIM] ' : '';
|
||||||
|
|
||||||
// Track conditionId → asset for win detection
|
// Track conditionId → { asset, token IDs } for win detection
|
||||||
conditionAssetMap.set(conditionId, asset.toLowerCase());
|
// yesTokenId = outcome 0 (clobTokenIds[0]), noTokenId = outcome 1 (clobTokenIds[1])
|
||||||
|
conditionInfoMap.set(conditionId, {
|
||||||
|
asset: asset.toLowerCase(),
|
||||||
|
yesTokenId,
|
||||||
|
noTokenId,
|
||||||
|
});
|
||||||
|
|
||||||
const sides = [
|
const sides = [
|
||||||
{ name: 'UP', tokenId: yesTokenId },
|
{ name: 'UP', tokenId: yesTokenId },
|
||||||
|
|||||||
+3
-2
@@ -17,8 +17,8 @@ import { initClient } from './services/client.js';
|
|||||||
import { getUsdcBalance } from './services/client.js';
|
import { getUsdcBalance } from './services/client.js';
|
||||||
import { initDashboard, appendLog, updateStatus, isDashboardActive } from './ui/dashboard.js';
|
import { initDashboard, appendLog, updateStatus, isDashboardActive } from './ui/dashboard.js';
|
||||||
import { startSniperDetector, stopSniperDetector } from './services/sniperDetector.js';
|
import { startSniperDetector, stopSniperDetector } from './services/sniperDetector.js';
|
||||||
import { executeSnipe, getActiveSnipes, getConditionAsset } from './services/sniperExecutor.js';
|
import { executeSnipe, getActiveSnipes, getConditionAsset, getConditionInfo } from './services/sniperExecutor.js';
|
||||||
import { redeemSniperPositions, onSniperWin } from './services/ctf.js';
|
import { redeemSniperPositions, onSniperWin, setSniperConditionLookup } from './services/ctf.js';
|
||||||
import { getSchedule, isAssetInSession, getNextSessionInfo } from './services/schedule.js';
|
import { getSchedule, isAssetInSession, getNextSessionInfo } from './services/schedule.js';
|
||||||
import { getTimeMultiplier } from './services/sniperSizing.js';
|
import { getTimeMultiplier } from './services/sniperSizing.js';
|
||||||
|
|
||||||
@@ -181,6 +181,7 @@ function tickPause(asset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSniperWin(handleWin);
|
onSniperWin(handleWin);
|
||||||
|
setSniperConditionLookup(getConditionInfo);
|
||||||
|
|
||||||
// ── Market handler ────────────────────────────────────────────────────────────
|
// ── Market handler ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -19,8 +19,8 @@ import config from './config/index.js';
|
|||||||
import logger from './utils/logger.js';
|
import logger from './utils/logger.js';
|
||||||
import { initClient } from './services/client.js';
|
import { initClient } from './services/client.js';
|
||||||
import { startSniperDetector, stopSniperDetector } from './services/sniperDetector.js';
|
import { startSniperDetector, stopSniperDetector } from './services/sniperDetector.js';
|
||||||
import { executeSnipe, getConditionAsset } from './services/sniperExecutor.js';
|
import { executeSnipe, getConditionAsset, getConditionInfo } from './services/sniperExecutor.js';
|
||||||
import { redeemSniperPositions, onSniperWin } from './services/ctf.js';
|
import { redeemSniperPositions, onSniperWin, setSniperConditionLookup } from './services/ctf.js';
|
||||||
import { getSchedule, isAssetInSession, getNextSessionInfo } from './services/schedule.js';
|
import { getSchedule, isAssetInSession, getNextSessionInfo } from './services/schedule.js';
|
||||||
import { getTimeMultiplier } from './services/sniperSizing.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);
|
onSniperWin(handleWin);
|
||||||
|
setSniperConditionLookup(getConditionInfo);
|
||||||
|
|
||||||
// ── Log session schedule ──────────────────────────────────────────────────────
|
// ── Log session schedule ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user