feat: full project overhaul — market maker, sniper, WebSocket watcher, terminal UI
- Rename project to polymarket-terminal - Add Market Maker bot (src/mm.js) with on-chain CTF split/merge/redeem via Gnosis Safe - Add Orderbook Sniper bot (src/sniper.js) with multi-asset GTC low-price orders - Add WebSocket watcher (src/services/wsWatcher.js) for real-time RTDS trade events - Add terminal dashboard UI (src/ui/dashboard.js) using blessed - Add CTF contract helpers (src/services/ctf.js) for splitPosition, mergePositions, redeemPositions - Add mmDetector, mmExecutor, sniperDetector, sniperExecutor services - Add simStats utility for dry-run P&L tracking - Translate all Indonesian-language strings to professional English across all files - Rewrite README.md in English with full setup guide, configuration reference, and architecture overview - Rewrite AGENT.MD in English as comprehensive AI agent and developer reference - Update package.json name, description, scripts, and keywords Co-Authored-By: direkturcrypto <direkturcrypto.x@mail3.me>
This commit is contained in:
+70
-40
@@ -1,6 +1,8 @@
|
||||
import { ethers } from 'ethers';
|
||||
import config from '../config/index.js';
|
||||
import { getOpenPositions, updatePosition, removePosition } from './position.js';
|
||||
import { getPolygonProvider } from './client.js';
|
||||
import { getOpenPositions, removePosition } from './position.js';
|
||||
import { recordSimResult } from '../utils/simStats.js';
|
||||
import logger from '../utils/logger.js';
|
||||
|
||||
// Contract addresses on Polygon
|
||||
@@ -17,13 +19,10 @@ const CTF_ABI = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a market has been resolved and our position is a winner
|
||||
* @param {string} conditionId
|
||||
* @returns {Object|null} { resolved, won }
|
||||
* Check if a market has been resolved via Gamma API
|
||||
*/
|
||||
async function checkMarketResolution(conditionId) {
|
||||
try {
|
||||
// Check via Gamma API
|
||||
const url = `${config.gammaHost}/markets?condition_id=${conditionId}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
@@ -35,8 +34,6 @@ async function checkMarketResolution(conditionId) {
|
||||
return {
|
||||
resolved: market.closed || market.resolved || false,
|
||||
active: market.active,
|
||||
endDate: market.end_date_iso,
|
||||
resolutionSource: market.resolution_source,
|
||||
question: market.question,
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -46,17 +43,17 @@ async function checkMarketResolution(conditionId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check on-chain if a position (token) has value (payout available)
|
||||
* Check on-chain payout fractions for a condition
|
||||
* Returns: { resolved: bool, payouts: [yes_fraction, no_fraction] }
|
||||
*/
|
||||
async function checkOnChainPayout(conditionId) {
|
||||
try {
|
||||
const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com');
|
||||
const provider = await getPolygonProvider();
|
||||
const ctf = new ethers.Contract(CTF_ADDRESS, CTF_ABI, provider);
|
||||
|
||||
const denominator = await ctf.payoutDenominator(conditionId);
|
||||
if (denominator.isZero()) return { resolved: false, payouts: [] };
|
||||
|
||||
// Check payouts for both outcomes (YES=0, NO=1)
|
||||
const payouts = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const numerator = await ctf.payoutNumerators(conditionId, i);
|
||||
@@ -64,27 +61,25 @@ async function checkOnChainPayout(conditionId) {
|
||||
}
|
||||
|
||||
return { resolved: true, payouts };
|
||||
} catch (err) {
|
||||
// If payoutDenominator is 0 or reverts, market not resolved
|
||||
} catch {
|
||||
return { resolved: false, payouts: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redeem winning position on-chain
|
||||
* Redeem winning position on-chain (real mode only)
|
||||
*/
|
||||
async function redeemPosition(conditionId, isNegRisk = false) {
|
||||
try {
|
||||
const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com');
|
||||
const provider = await getPolygonProvider();
|
||||
const wallet = new ethers.Wallet(config.privateKey, provider);
|
||||
const ctfAddress = isNegRisk ? NEG_RISK_CTF_ADDRESS : CTF_ADDRESS;
|
||||
const ctf = new ethers.Contract(ctfAddress, CTF_ABI, wallet);
|
||||
|
||||
const parentCollectionId = ethers.constants.HashZero;
|
||||
const indexSets = [1, 2]; // Both outcomes
|
||||
|
||||
logger.info(`Redeeming position for conditionId: ${conditionId}`);
|
||||
const indexSets = [1, 2];
|
||||
|
||||
logger.info(`Redeeming position: ${conditionId}`);
|
||||
const tx = await ctf.redeemPositions(
|
||||
USDC_ADDRESS,
|
||||
parentCollectionId,
|
||||
@@ -93,56 +88,91 @@ async function redeemPosition(conditionId, isNegRisk = false) {
|
||||
{ gasLimit: 300000 },
|
||||
);
|
||||
|
||||
logger.info(`Redeem tx sent: ${tx.hash}`);
|
||||
logger.info(`Redeem tx: ${tx.hash}`);
|
||||
const receipt = await tx.wait();
|
||||
logger.success(`Redeem confirmed in block ${receipt.blockNumber}`);
|
||||
|
||||
logger.success(`Redeemed in block ${receipt.blockNumber}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error('Failed to redeem position:', err.message);
|
||||
logger.error('Failed to redeem:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all open positions for redeemable (resolved & won) markets
|
||||
* Simulate redemption: determine win/loss and record stats
|
||||
*/
|
||||
async function simulateRedeem(position) {
|
||||
// Need on-chain payout to know who actually won
|
||||
const onChain = await checkOnChainPayout(position.conditionId);
|
||||
|
||||
if (!onChain.resolved) {
|
||||
logger.info(`[SIM] Market resolved via API but payout not on-chain yet: ${position.market}`);
|
||||
return false; // check again next interval
|
||||
}
|
||||
|
||||
// outcome index: YES = 0, NO = 1
|
||||
const outcomeStr = (position.outcome || 'yes').toLowerCase();
|
||||
const outcomeIdx = outcomeStr === 'yes' ? 0 : 1;
|
||||
const payoutFraction = onChain.payouts[outcomeIdx] ?? 0;
|
||||
|
||||
// In Polymarket, winning shares redeem at $1 each
|
||||
const returned = payoutFraction * position.shares;
|
||||
const pnl = returned - position.totalCost;
|
||||
|
||||
if (payoutFraction > 0) {
|
||||
logger.money(
|
||||
`[SIM] WIN! "${position.market}" | ${position.outcome} won` +
|
||||
` | +$${pnl.toFixed(2)} (+${((pnl / position.totalCost) * 100).toFixed(1)}%)`,
|
||||
);
|
||||
recordSimResult(position, 'WIN', pnl, returned);
|
||||
} else {
|
||||
logger.error(
|
||||
`[SIM] LOSS: "${position.market}" | ${position.outcome} lost` +
|
||||
` | -$${position.totalCost.toFixed(2)} (-100%)`,
|
||||
);
|
||||
recordSimResult(position, 'LOSS', pnl, returned);
|
||||
}
|
||||
|
||||
removePosition(position.conditionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all open positions for resolved markets and redeem/simulate
|
||||
*/
|
||||
export async function checkAndRedeemPositions() {
|
||||
const positions = getOpenPositions();
|
||||
if (positions.length === 0) return;
|
||||
|
||||
logger.info(`Checking ${positions.length} position(s) for redemption...`);
|
||||
logger.info(`Checking ${positions.length} position(s) for resolution...`);
|
||||
|
||||
for (const position of positions) {
|
||||
try {
|
||||
// Check via API first
|
||||
// 1. Check via Gamma API
|
||||
const resolution = await checkMarketResolution(position.conditionId);
|
||||
const isResolved = resolution?.resolved;
|
||||
|
||||
if (!resolution || !resolution.resolved) {
|
||||
// Try on-chain check as fallback
|
||||
if (!isResolved) {
|
||||
// 2. Fallback: on-chain check
|
||||
const onChain = await checkOnChainPayout(position.conditionId);
|
||||
if (!onChain.resolved) continue;
|
||||
|
||||
// Check if our outcome won
|
||||
// Determine outcome index (0=YES, 1=NO based on token position)
|
||||
logger.info(`Market resolved on-chain: ${position.market} | Payouts: ${onChain.payouts}`);
|
||||
logger.info(`Market resolved on-chain: ${position.market}`);
|
||||
} else {
|
||||
logger.info(`Market resolved: ${position.market}`);
|
||||
}
|
||||
|
||||
// 3. Simulate or execute real redeem
|
||||
if (config.dryRun) {
|
||||
logger.info(`[DRY RUN] Would redeem position: ${position.market}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt to redeem
|
||||
const success = await redeemPosition(position.conditionId);
|
||||
if (success) {
|
||||
removePosition(position.conditionId);
|
||||
logger.money(`Redeemed: ${position.market}`);
|
||||
await simulateRedeem(position);
|
||||
} else {
|
||||
const success = await redeemPosition(position.conditionId);
|
||||
if (success) {
|
||||
removePosition(position.conditionId);
|
||||
logger.money(`Redeemed: ${position.market}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error checking position ${position.market}:`, err.message);
|
||||
logger.error(`Error checking ${position.market}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user