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:
direkturcrypto
2026-02-23 23:03:06 +07:00
parent 7c7fad45f3
commit 526076fe6e
22 changed files with 3485 additions and 558 deletions
+85 -49
View File
@@ -4,19 +4,22 @@ import { getClient, getUsdcBalance } from './client.js';
import { hasPosition, addPosition, getPosition, updatePosition, removePosition } from './position.js';
import { fetchMarketByTokenId } from './watcher.js';
import { placeAutoSell } from './autoSell.js';
import { recordSimBuy } from '../utils/simStats.js';
import logger from '../utils/logger.js';
/**
* Calculate trade size based on settings
* @param {number} traderSize - Trader's trade size in USDC
* @returns {number} Our trade size in USDC
* Calculate trade size for our entry — independent of the individual fill event.
*
* Limit orders can be filled in many small chunks; using the event's fill size
* would give inconsistent (often sub-minimum) results.
*
* SIZE_MODE=percentage → SIZE_PERCENT% of MAX_POSITION_SIZE per market
* SIZE_MODE=balance → SIZE_PERCENT% of our current USDC.e balance
*/
async function calculateTradeSize(traderSize) {
async function calculateTradeSize() {
if (config.sizeMode === 'percentage') {
// % of trader's trade size
return traderSize * (config.sizePercent / 100);
return config.maxPositionSize * (config.sizePercent / 100);
} else if (config.sizeMode === 'balance') {
// % of our own balance
const balance = await getUsdcBalance();
return balance * (config.sizePercent / 100);
}
@@ -61,14 +64,32 @@ async function getMarketOptions(tokenId) {
export async function executeBuy(trade) {
const { tokenId, conditionId, market, price, size } = trade;
// Check if already have position for this market
if (hasPosition(conditionId)) {
logger.warn(`Already have position for: ${market || conditionId}. Skipping buy.`);
return;
// Get market options first to resolve conditionId
const marketOpts = await getMarketOptions(tokenId);
const effectiveConditionId = conditionId || marketOpts.conditionId;
// Check existing position and max position size cap
const existingPos = getPosition(effectiveConditionId);
if (existingPos) {
const spent = existingPos.totalCost || 0;
if (spent >= config.maxPositionSize) {
logger.warn(`Max position $${config.maxPositionSize} reached for: ${market || effectiveConditionId} (spent $${spent.toFixed(2)}). Skipping.`);
return;
}
logger.info(`Adding to existing position (spent $${spent.toFixed(2)} / $${config.maxPositionSize})`);
}
// Calculate our trade size (independent of individual fill event)
let tradeSize = await calculateTradeSize();
// Cap so we don't exceed maxPositionSize
if (existingPos) {
const remaining = config.maxPositionSize - (existingPos.totalCost || 0);
tradeSize = Math.min(tradeSize, remaining);
} else {
tradeSize = Math.min(tradeSize, config.maxPositionSize);
}
// Calculate our trade size
const tradeSize = await calculateTradeSize(size * price); // trader's USDC amount
if (tradeSize < config.minTradeSize) {
logger.warn(`Trade size $${tradeSize.toFixed(2)} below minimum $${config.minTradeSize}. Skipping.`);
return;
@@ -81,30 +102,32 @@ export async function executeBuy(trade) {
return;
}
// Get market options
const marketOpts = await getMarketOptions(tokenId);
const effectiveConditionId = conditionId || marketOpts.conditionId;
// Double check no position exists
if (effectiveConditionId && hasPosition(effectiveConditionId)) {
logger.warn(`Already have position for: ${market || effectiveConditionId}. Skipping buy.`);
return;
}
logger.trade(`BUY ${market || tokenId} | Size: $${tradeSize.toFixed(2)} | Trader price: ${price}`);
if (config.dryRun) {
logger.info('[DRY RUN] Would place market buy order');
// Still record position in dry run for testing
addPosition({
conditionId: effectiveConditionId,
tokenId,
market: market || marketOpts.question || tokenId,
shares: tradeSize / price,
avgBuyPrice: price,
totalCost: tradeSize,
outcome: trade.outcome,
});
logger.trade(`[SIM] BUY ${market || tokenId} | $${tradeSize.toFixed(2)} @ $${price} | outcome: ${trade.outcome || '?'}`);
const dryShares = tradeSize / price;
if (existingPos) {
const newShares = existingPos.shares + dryShares;
const newTotalCost = existingPos.totalCost + tradeSize;
updatePosition(effectiveConditionId, {
shares: newShares,
avgBuyPrice: newTotalCost / newShares,
totalCost: newTotalCost,
});
logger.info(`[SIM] Position accumulated: $${newTotalCost.toFixed(2)} / $${config.maxPositionSize}`);
} else {
addPosition({
conditionId: effectiveConditionId,
tokenId,
market: market || marketOpts.question || tokenId,
shares: dryShares,
avgBuyPrice: price,
totalCost: tradeSize,
outcome: trade.outcome,
});
}
recordSimBuy();
return;
}
@@ -172,23 +195,36 @@ export async function executeBuy(trade) {
return;
}
// Calculate avg buy price
const avgBuyPrice = totalSharesFilled > 0 ? totalCostFilled / totalSharesFilled : price;
// Calculate avg buy price for this fill
const fillAvgPrice = totalSharesFilled > 0 ? totalCostFilled / totalSharesFilled : price;
// Record position
addPosition({
conditionId: effectiveConditionId,
tokenId,
market: market || marketOpts.question || tokenId,
shares: totalSharesFilled,
avgBuyPrice,
totalCost: totalCostFilled,
outcome: trade.outcome,
});
if (existingPos) {
// Accumulate into existing position (weighted avg price)
const newShares = existingPos.shares + totalSharesFilled;
const newTotalCost = existingPos.totalCost + totalCostFilled;
const newAvgBuyPrice = newTotalCost / newShares;
updatePosition(effectiveConditionId, {
shares: newShares,
avgBuyPrice: newAvgBuyPrice,
totalCost: newTotalCost,
});
logger.success(`Position updated: ${existingPos.market} | total $${newTotalCost.toFixed(2)} / $${config.maxPositionSize}`);
} else {
// New position
addPosition({
conditionId: effectiveConditionId,
tokenId,
market: market || marketOpts.question || tokenId,
shares: totalSharesFilled,
avgBuyPrice: fillAvgPrice,
totalCost: totalCostFilled,
outcome: trade.outcome,
});
// Auto-sell if enabled
if (config.autoSellEnabled) {
await placeAutoSell(effectiveConditionId, tokenId, totalSharesFilled, avgBuyPrice, marketOpts);
// Auto-sell only on initial entry, not on accumulation
if (config.autoSellEnabled) {
await placeAutoSell(effectiveConditionId, tokenId, totalSharesFilled, fillAvgPrice, marketOpts);
}
}
}