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
+48 -22
View File
@@ -1,34 +1,60 @@
const COLORS = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
// ANSI color codes (used in normal terminal mode)
const A = {
reset: '\x1b[0m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
cyan: '\x1b[36m',
};
function timestamp() {
// Blessed tag pairs (used when dashboard is active)
const B = {
red: ['{red-fg}', '{/red-fg}'],
green: ['{green-fg}', '{/green-fg}'],
yellow: ['{yellow-fg}', '{/yellow-fg}'],
blue: ['{blue-fg}', '{/blue-fg}'],
magenta: ['{magenta-fg}', '{/magenta-fg}'],
cyan: ['{cyan-fg}', '{/cyan-fg}'],
};
let outputFn = null; // When set, all log goes here (blessed dashboard mode)
function ts() {
return new Date().toISOString().replace('T', ' ').substring(0, 19);
}
function formatMsg(level, color, emoji, ...args) {
const ts = timestamp();
const prefix = `${COLORS.dim}[${ts}]${COLORS.reset} ${color}${emoji} ${level}${COLORS.reset}`;
console.log(prefix, ...args);
function stringify(args) {
return args.map((a) => (a && typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
}
function log(ansiColor, bColor, emoji, level, ...args) {
const msg = stringify(args);
if (outputFn) {
const [open, close] = bColor;
outputFn(`{gray-fg}[${ts()}]{/gray-fg} ${open}${emoji} ${level}${close} ${msg}`);
} else {
process.stdout.write(
`${A.dim}[${ts()}]${A.reset} ${ansiColor}${emoji} ${level}${A.reset} ${msg}\n`,
);
}
}
const logger = {
info: (...args) => formatMsg('INFO', COLORS.blue, '️ ', ...args),
success: (...args) => formatMsg('SUCCESS', COLORS.green, '✅', ...args),
warn: (...args) => formatMsg('WARN', COLORS.yellow, '⚠️ ', ...args),
error: (...args) => formatMsg('ERROR', COLORS.red, '❌', ...args),
trade: (...args) => formatMsg('TRADE', COLORS.magenta, '📊', ...args),
watch: (...args) => formatMsg('WATCH', COLORS.cyan, '👀', ...args),
money: (...args) => formatMsg('MONEY', COLORS.green, '💰', ...args),
info: (...a) => log(A.blue, B.blue, '️ ', 'INFO', ...a),
success: (...a) => log(A.green, B.green, '✅', 'SUCCESS', ...a),
warn: (...a) => log(A.yellow, B.yellow, '⚠️ ', 'WARN', ...a),
error: (...a) => log(A.red, B.red, '❌', 'ERROR', ...a),
trade: (...a) => log(A.magenta, B.magenta, '📊', 'TRADE', ...a),
watch: (...a) => log(A.cyan, B.cyan, '👀', 'WATCH', ...a),
money: (...a) => log(A.green, B.green, '💰', 'MONEY', ...a),
/** Call once after initDashboard() to redirect all logs to the TUI */
setOutput(fn) {
outputFn = fn;
},
};
export default logger;
+63
View File
@@ -0,0 +1,63 @@
import { readState, writeState } from './state.js';
const SIM_FILE = 'sim_stats.json';
function defaultStats() {
return {
startTime: new Date().toISOString(),
totalBuys: 0,
totalResolved: 0,
wins: 0,
losses: 0,
closedPnl: 0,
closedPositions: [],
};
}
export function getSimStats() {
return readState(SIM_FILE, defaultStats());
}
export function recordSimBuy() {
const stats = getSimStats();
stats.totalBuys = (stats.totalBuys || 0) + 1;
writeState(SIM_FILE, stats);
}
/**
* Record result of a resolved simulation position
* @param {Object} position - the position object
* @param {'WIN'|'LOSS'} result
* @param {number} pnl - realized P&L in USDC
* @param {number} returned - USDC returned
*/
export function recordSimResult(position, result, pnl, returned) {
const stats = getSimStats();
stats.totalResolved = (stats.totalResolved || 0) + 1;
if (result === 'WIN') stats.wins = (stats.wins || 0) + 1;
else stats.losses = (stats.losses || 0) + 1;
stats.closedPnl = ((stats.closedPnl || 0) + pnl);
stats.closedPositions = stats.closedPositions || [];
stats.closedPositions.push({
market: position.market,
outcome: position.outcome,
totalCost: position.totalCost,
shares: position.shares,
returned,
pnl,
result,
closedAt: new Date().toISOString(),
});
// Keep last 100 entries
if (stats.closedPositions.length > 100) {
stats.closedPositions = stats.closedPositions.slice(-100);
}
writeState(SIM_FILE, stats);
}
export function resetSimStats() {
writeState(SIM_FILE, defaultStats());
}