2026-02-22 15:38:13 +07:00
|
|
|
import dotenv from 'dotenv';
|
|
|
|
|
dotenv.config();
|
|
|
|
|
|
|
|
|
|
const config = {
|
|
|
|
|
// Wallet
|
2026-02-23 23:03:06 +07:00
|
|
|
privateKey: process.env.PRIVATE_KEY, // EOA private key (for signing only)
|
|
|
|
|
proxyWallet: process.env.PROXY_WALLET_ADDRESS, // Polymarket proxy wallet (deposit USDC here)
|
2026-02-22 15:38:13 +07:00
|
|
|
|
|
|
|
|
// Polymarket API (optional, auto-derived if empty)
|
|
|
|
|
clobApiKey: process.env.CLOB_API_KEY || '',
|
|
|
|
|
clobApiSecret: process.env.CLOB_API_SECRET || '',
|
|
|
|
|
clobApiPassphrase: process.env.CLOB_API_PASSPHRASE || '',
|
|
|
|
|
|
|
|
|
|
// Polymarket endpoints
|
|
|
|
|
clobHost: 'https://clob.polymarket.com',
|
|
|
|
|
gammaHost: 'https://gamma-api.polymarket.com',
|
|
|
|
|
dataHost: 'https://data-api.polymarket.com',
|
|
|
|
|
chainId: 137,
|
|
|
|
|
|
2026-02-23 23:03:06 +07:00
|
|
|
// Polygon RPC
|
|
|
|
|
polygonRpcUrl: process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com',
|
|
|
|
|
|
2026-02-22 15:38:13 +07:00
|
|
|
// Trader to copy
|
|
|
|
|
traderAddress: process.env.TRADER_ADDRESS,
|
|
|
|
|
|
|
|
|
|
// Trade sizing
|
|
|
|
|
sizeMode: process.env.SIZE_MODE || 'percentage', // "percentage" | "balance"
|
|
|
|
|
sizePercent: parseFloat(process.env.SIZE_PERCENT || '50'),
|
|
|
|
|
minTradeSize: parseFloat(process.env.MIN_TRADE_SIZE || '1'),
|
2026-02-23 23:03:06 +07:00
|
|
|
maxPositionSize: parseFloat(process.env.MAX_POSITION_SIZE || '10'),
|
2026-02-22 15:38:13 +07:00
|
|
|
|
|
|
|
|
// Auto sell
|
|
|
|
|
autoSellEnabled: process.env.AUTO_SELL_ENABLED === 'true',
|
|
|
|
|
autoSellProfitPercent: parseFloat(process.env.AUTO_SELL_PROFIT_PERCENT || '10'),
|
|
|
|
|
|
|
|
|
|
// Sell mode when copying sell
|
|
|
|
|
sellMode: process.env.SELL_MODE || 'market', // "market" | "limit"
|
|
|
|
|
|
2026-02-23 23:03:06 +07:00
|
|
|
// Redeem interval (seconds)
|
2026-02-22 15:38:13 +07:00
|
|
|
redeemInterval: parseInt(process.env.REDEEM_INTERVAL || '60', 10) * 1000,
|
|
|
|
|
|
|
|
|
|
// Dry run
|
|
|
|
|
dryRun: process.env.DRY_RUN === 'true',
|
|
|
|
|
|
|
|
|
|
// Retry settings
|
|
|
|
|
maxRetries: 5,
|
|
|
|
|
retryDelay: 3000,
|
2026-02-23 23:03:06 +07:00
|
|
|
|
2026-02-25 03:18:57 +07:00
|
|
|
// Skip buy if market closes within this many seconds (default 5 minutes)
|
|
|
|
|
minMarketTimeLeft: parseInt(process.env.MIN_MARKET_TIME_LEFT || '300', 10),
|
|
|
|
|
|
2026-02-23 23:03:06 +07:00
|
|
|
// ── Market Maker ──────────────────────────────────────────────
|
|
|
|
|
mmAssets: (process.env.MM_ASSETS || 'btc')
|
|
|
|
|
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
|
|
|
|
mmDuration: process.env.MM_DURATION || '5m', // '5m' or '15m'
|
|
|
|
|
mmTradeSize: parseFloat(process.env.MM_TRADE_SIZE || '5'), // USDC per side
|
|
|
|
|
mmSellPrice: parseFloat(process.env.MM_SELL_PRICE || '0.60'), // limit sell target
|
|
|
|
|
mmCutLossTime: parseInt( process.env.MM_CUT_LOSS_TIME || '60', 10), // seconds before close
|
|
|
|
|
mmMarketKeyword: process.env.MM_MARKET_KEYWORD || 'Bitcoin Up or Down',
|
|
|
|
|
mmEntryWindow: parseInt( process.env.MM_ENTRY_WINDOW || '45', 10), // max secs after open
|
|
|
|
|
mmPollInterval: parseInt( process.env.MM_POLL_INTERVAL || '10', 10) * 1000,
|
|
|
|
|
|
|
|
|
|
// ── Recovery Buy (after cut-loss) ─────────────────────────────
|
|
|
|
|
// When enabled: after cutting loss, monitor prices for 10s and
|
|
|
|
|
// market-buy the dominant side if it's above threshold and rising/stable.
|
|
|
|
|
mmRecoveryBuy: process.env.MM_RECOVERY_BUY === 'true',
|
|
|
|
|
mmRecoveryThreshold: parseFloat(process.env.MM_RECOVERY_THRESHOLD || '0.70'), // min price to qualify
|
|
|
|
|
mmRecoverySize: parseFloat(process.env.MM_RECOVERY_SIZE || '0'), // 0 = use mmTradeSize
|
|
|
|
|
|
|
|
|
|
// ── Orderbook Sniper ───────────────────────────────────────────
|
|
|
|
|
// Places tiny GTC limit BUY orders at a very low price on each side
|
|
|
|
|
// of ETH/SOL/XRP 5-minute markets — catches panic dumps near $0.
|
|
|
|
|
sniperAssets: (process.env.SNIPER_ASSETS || 'eth,sol,xrp')
|
|
|
|
|
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
|
|
|
|
sniperPrice: parseFloat(process.env.SNIPER_PRICE || '0.01'), // $ per share
|
|
|
|
|
sniperShares: parseFloat(process.env.SNIPER_SHARES || '5'), // shares per side
|
2026-02-22 15:38:13 +07:00
|
|
|
};
|
|
|
|
|
|
2026-02-23 23:03:06 +07:00
|
|
|
// Validation for copy-trade bot
|
2026-02-22 15:38:13 +07:00
|
|
|
export function validateConfig() {
|
2026-02-23 23:03:06 +07:00
|
|
|
const required = ['privateKey', 'proxyWallet', 'traderAddress'];
|
2026-02-22 15:38:13 +07:00
|
|
|
const missing = required.filter((key) => !config[key]);
|
|
|
|
|
if (missing.length > 0) {
|
|
|
|
|
throw new Error(`Missing required config: ${missing.join(', ')}. Check your .env file.`);
|
|
|
|
|
}
|
|
|
|
|
if (!['percentage', 'balance'].includes(config.sizeMode)) {
|
|
|
|
|
throw new Error(`Invalid SIZE_MODE: ${config.sizeMode}. Use "percentage" or "balance".`);
|
|
|
|
|
}
|
|
|
|
|
if (!['market', 'limit'].includes(config.sellMode)) {
|
|
|
|
|
throw new Error(`Invalid SELL_MODE: ${config.sellMode}. Use "market" or "limit".`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 23:03:06 +07:00
|
|
|
// Validation for market-maker bot
|
|
|
|
|
export function validateMMConfig() {
|
|
|
|
|
const required = ['privateKey', 'proxyWallet'];
|
|
|
|
|
const missing = required.filter((key) => !config[key]);
|
|
|
|
|
if (missing.length > 0) {
|
|
|
|
|
throw new Error(`Missing required config: ${missing.join(', ')}. Check your .env file.`);
|
|
|
|
|
}
|
|
|
|
|
if (config.mmTradeSize <= 0) throw new Error('MM_TRADE_SIZE must be > 0');
|
|
|
|
|
if (config.mmSellPrice <= 0 || config.mmSellPrice >= 1)
|
|
|
|
|
throw new Error('MM_SELL_PRICE must be between 0 and 1');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:38:13 +07:00
|
|
|
export default config;
|