feat: add time-based multiplier sizing, pause-after-win, and outcome-based win detection

- Time-based bet sizing multiplier (SNIPER_MULTIPLIERS, UTC+8 windows)
- Pause N rounds per asset after win (SNIPER_PAUSE_ROUNDS_AFTER_WIN)
- Win detection via payoutNumerators outcome check instead of redeem value threshold

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
direkturcrypto
2026-03-11 12:53:48 +07:00
co-authored by Claude Opus 4.6
parent 9e78fb1bfe
commit a8f28c961b
7 changed files with 268 additions and 19 deletions
+29 -9
View File
@@ -542,6 +542,17 @@ export async function redeemMMPositions() {
const _failedConditions = new Set();
const _skippedLosses = new Set();
// Callback invoked when a win is detected — receives conditionId
let _onWinCallback = null;
/**
* Register a callback to be called when a sniper win is detected.
* Callback signature: (conditionId: string) => void
*/
export function onSniperWin(cb) {
_onWinCallback = cb;
}
/**
* Redeem sniper positions via Gnosis Safe.
* Only redeems WINNING positions — skip losses (they can be manually cleared).
@@ -616,37 +627,46 @@ export async function redeemSniperPositions() {
continue;
}
// Estimate payout from numerators (for logging)
const payoutFractions = await Promise.all(
// Check outcome via payoutNumerators — which outcome index won?
const payoutNums = await Promise.all(
[0, 1].map((i) =>
ctf.payoutNumerators(conditionId, i)
.then((n) => n.toNumber() / denominator.toNumber())
ctf.payoutNumerators(conditionId, i).then((n) => n.toNumber())
)
);
const denom = denominator.toNumber();
const payoutFractions = payoutNums.map((n) => n / denom);
// Determine winning outcome index (the one with payoutFraction > 0)
const winningOutcome = payoutFractions[0] > 0 ? 0 : payoutFractions[1] > 0 ? 1 : -1;
// Win = we hold shares on the winning outcome side
const isWin = winningOutcome >= 0 && balances[winningOutcome] > 0;
const expectedUsdc = balances.reduce(
(sum, shares, i) => sum + shares * (payoutFractions[i] ?? 0), 0
);
const label = conditionId.slice(0, 12) + '...';
const isWin = expectedUsdc >= 0.01;
// 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 (cached, no future checks)`);
logger.info(`SNIPER[SIM] skip loss: ${label}${totalShares.toFixed(3)} shares, outcome=${winningOutcome} (cached)`);
} else {
logger.info(`SNIPER redeemer: skip loss ${label}cached for future cycles`);
logger.info(`SNIPER redeemer: skip loss ${label}outcome=${winningOutcome}, no shares on winner`);
}
continue;
}
// Track win for pause-after-win (notify via callback)
if (_onWinCallback) _onWinCallback(conditionId);
if (config.dryRun) {
logger.money(`SNIPER[SIM] redeem: ${label}${totalShares.toFixed(3)} shares → ~$${expectedUsdc.toFixed(2)} USDC (WIN)`);
logger.money(`SNIPER[SIM] redeem: ${label}${balances[winningOutcome].toFixed(3)} shares on outcome ${winningOutcome} → ~$${expectedUsdc.toFixed(2)} USDC (WIN)`);
continue;
}
logger.info(`SNIPER redeemer: ${label} resolved WIN — ${totalShares.toFixed(3)} shares → ~$${expectedUsdc.toFixed(2)} USDC`);
logger.info(`SNIPER redeemer: ${label} resolved WIN — outcome ${winningOutcome}, ${balances[winningOutcome].toFixed(3)} shares → ~$${expectedUsdc.toFixed(2)} USDC`);
// Call redeemPositions through Safe — winners only
const data = ctfIface.encodeFunctionData('redeemPositions', [