From c524ed92ef611a1b47d53f8b0a910847b4b007b0 Mon Sep 17 00:00:00 2001 From: direkturcrypto Date: Tue, 3 Mar 2026 14:52:37 +0700 Subject: [PATCH] feat: implement 3-tier sniper strategy with weighted sizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace single price/shares with 3-tier system (3c/2c/1c) - Tier allocation: 20%/30%/50% (high→low price) - Min 5 shares per tier enforced - Update .env.example with new config vars - Update README.md with strategy documentation Co-Authored-By: Claude Opus 4.6 --- .env.example | 18 +++-- README.md | 22 +++++- src/config/index.js | 14 +++- src/services/sniperExecutor.js | 136 ++++++++++++++++++++------------- src/sniper-tui.js | 7 +- src/sniper.js | 8 +- 6 files changed, 133 insertions(+), 72 deletions(-) diff --git a/.env.example b/.env.example index 8d3dfc3..813283d 100644 --- a/.env.example +++ b/.env.example @@ -135,18 +135,22 @@ MM_ADAPTIVE_MONITOR_SEC=5 # ───────────────────────────────────────────── # ORDERBOOK SNIPER (sniper.js / npm run sniper-sim) -# Places tiny GTC BUY orders at a low price on both sides of -# ETH/SOL/XRP 5-minute markets — catches panic dumps near $0. +# 3-Tier Strategy: Places GTC BUY orders at 3 prices with weighted sizing +# Tier 1 (3c): smallest size | Tier 2 (2c): medium | Tier 3 (1c): largest +# Catches panic dumps at multiple price levels with optimal capital allocation # ───────────────────────────────────────────── # Comma-separated assets to snipe SNIPER_ASSETS=eth,sol,xrp -# Buy price per share (1 cent = $0.01) -SNIPER_PRICE=0.01 +# 3-Tier pricing (high to low) +SNIPER_TIER1_PRICE=0.03 # Highest price, smallest allocation (20%) +SNIPER_TIER2_PRICE=0.02 # Mid price, medium allocation (30%) +SNIPER_TIER3_PRICE=0.01 # Lowest price, largest allocation (50%) -# Shares per side — minimum Polymarket order size is 5 shares -# At $0.01/share: 5 shares = $0.05 per side, $0.10 per market -SNIPER_SHARES=5 +# Max total shares per side (min 5 shares per tier) +# Example: 15 shares → 3@3c + 5@2c + 7@1c +# Example: 30 shares → 6@3c + 9@2c + 15@1c +SNIPER_MAX_SHARES=15 # ── Sniper Session Schedule (all times UTC+8) ────────────── # Format: HH:MM-HH:MM,HH:MM-HH:MM (comma-separated sessions) diff --git a/README.md b/README.md index 2fe2cfb..1e58976 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,10 @@ - **Simulation Mode** — Full dry-run with P&L tracking ### Orderbook Sniper Bot -- **Low-Price Orders** — Places tiny GTC BUY orders at a configurable price (e.g. $0.01) on both sides +- **3-Tier Strategy** — Places GTC BUY orders at 3c, 2c, and 1c with weighted sizing (20%/30%/50%) - **Multi-Asset** — Targets ETH, SOL, XRP, and more simultaneously - **Simulation Mode** — Preview orders without spending funds +- **Session Scheduling** — Per-asset time windows (UTC+8) for selective trading --- @@ -139,11 +140,26 @@ Leave these blank to have the client auto-derive credentials from your private k ### Orderbook Sniper Settings +**3-Tier Strategy:** Places orders at 3 price levels with weighted sizing + | Variable | Description | Default | |---|---|---| | `SNIPER_ASSETS` | Comma-separated assets to snipe (e.g. `eth,sol,xrp`) | `eth,sol,xrp` | -| `SNIPER_PRICE` | Buy price per share (e.g. `0.01` = $0.01) | `0.01` | -| `SNIPER_SHARES` | Shares per side (minimum 5 per Polymarket rules) | `5` | +| `SNIPER_TIER1_PRICE` | Highest price tier (e.g. `0.03` = 3c) | `0.03` | +| `SNIPER_TIER2_PRICE` | Mid price tier (e.g. `0.02` = 2c) | `0.02` | +| `SNIPER_TIER3_PRICE` | Lowest price tier (e.g. `0.01` = 1c) | `0.01` | +| `SNIPER_MAX_SHARES` | Max total shares per side (min 5 per tier) | `15` | + +**Allocation:** +- Tier 1 (3c): 20% of max shares (min 5) +- Tier 2 (2c): 30% of max shares (min 5) +- Tier 3 (1c): 50% of max shares (min 5) + +**Example with `SNIPER_MAX_SHARES=15`:** +- 3 shares @ 3c = $0.09 +- 5 shares @ 2c = $0.10 +- 7 shares @ 1c = $0.07 +- **Total per side:** 15 shares = $0.26 --- diff --git a/src/config/index.js b/src/config/index.js index 0f1cb4d..06b84d8 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -75,12 +75,18 @@ const config = { 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. + // 3-tier strategy: places GTC limit BUY orders at 3c, 2c, and 1c + // Tier 1 (3c): smallest size | Tier 2 (2c): medium size | Tier 3 (1c): largest size + // Min 5 shares per tier, total = SNIPER_MAX_SHARES_PER_SIDE 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 + sniperTierPrices: [ + parseFloat(process.env.SNIPER_TIER1_PRICE || '0.03'), // high price, small size + parseFloat(process.env.SNIPER_TIER2_PRICE || '0.02'), // mid price, medium size + parseFloat(process.env.SNIPER_TIER3_PRICE || '0.01'), // low price, large size + ], + sniperMaxShares: parseFloat(process.env.SNIPER_MAX_SHARES || '15'), // max total per side + sniperMinSharesPerTier: 5, // minimum shares for each tier // ── Sniper Schedule (UTC+8) ──────────────────────────────────── // Per-asset session windows. Format: SNIPER_SCHEDULE_{ASSET}=HH:MM-HH:MM,HH:MM-HH:MM diff --git a/src/services/sniperExecutor.js b/src/services/sniperExecutor.js index e8a5e43..c2c8937 100644 --- a/src/services/sniperExecutor.js +++ b/src/services/sniperExecutor.js @@ -1,17 +1,10 @@ /** * sniperExecutor.js - * Places GTC limit BUY orders at a very low price on both sides of a market. - * - * Strategy: - * - For each market detected by sniperDetector, place two GTC BUY orders: - * UP token at $SNIPER_PRICE × SNIPER_SHARES shares - * DOWN token at $SNIPER_PRICE × SNIPER_SHARES shares - * - Orders sit in the orderbook. If someone panic-dumps below the price, - * the order fills and becomes redeemable if that side wins. - * - GTC orders expire automatically when the market closes — no cleanup needed. - * - * Cost per market: SNIPER_PRICE × SNIPER_SHARES × 2 sides - * e.g. $0.01 × 5 × 2 = $0.10 per market, $0.30 for 3 assets per 5-min slot + * 3-Tier Sniper Strategy: + * - Tier 1: 3c price, smallest size (20% of max) + * - Tier 2: 2c price, medium size (30% of max) + * - Tier 3: 1c price, largest size (50% of max) + * Min 5 shares per tier, total = SNIPER_MAX_SHARES */ import { Side, OrderType } from '@polymarket/clob-client'; @@ -19,13 +12,37 @@ import config from '../config/index.js'; import { getClient } from './client.js'; import logger from '../utils/logger.js'; -// In-memory tracking of placed snipe orders (for TUI status panel) -const activeSnipes = []; // { asset, side, question, orderId, price, shares, cost, potentialPayout } +// In-memory tracking of placed snipe orders +const activeSnipes = []; export function getActiveSnipes() { return [...activeSnipes]; } +/** + * Calculate tier sizes based on max shares. + * Distribution: 20% | 30% | 50% (high→low price) + * Minimum 5 shares per tier. + */ +function calculateTierSizes(maxShares, minPerTier) { + // Distribution percentages + const ratios = [0.20, 0.30, 0.50]; // Tier 1, 2, 3 + + const sizes = ratios.map(ratio => { + const size = Math.floor(maxShares * ratio); + return Math.max(size, minPerTier); + }); + + // Ensure we don't exceed maxShares after rounding up to minimums + const total = sizes.reduce((a, b) => a + b, 0); + if (total > maxShares) { + // Adjust tier 3 (largest) down if needed + sizes[2] = Math.max(minPerTier, sizes[2] - (total - maxShares)); + } + + return sizes; +} + export async function executeSnipe(market) { const { asset, conditionId, question, yesTokenId, noTokenId, tickSize, negRisk } = market; const label = question.slice(0, 40); @@ -36,56 +53,67 @@ export async function executeSnipe(market) { { name: 'DOWN', tokenId: noTokenId }, ]; - logger.info(`SNIPER: ${sim}${asset.toUpperCase()} — "${label}" | $${config.sniperPrice} × ${config.sniperShares}sh each side`); + const prices = config.sniperTierPrices; + const sizes = calculateTierSizes(config.sniperMaxShares, config.sniperMinSharesPerTier); + + logger.info(`SNIPER: ${sim}${asset.toUpperCase()} — "${label}" | 3-tier: 3c×${sizes[0]} | 2c×${sizes[1]} | 1c×${sizes[2]}`); for (const { name, tokenId } of sides) { - if (config.dryRun) { - const cost = config.sniperPrice * config.sniperShares; - logger.trade(`SNIPER[SIM]: ${asset.toUpperCase()} ${name} @ $${config.sniperPrice} × ${config.sniperShares}sh | cost $${cost.toFixed(3)} | payout $${config.sniperShares} if wins`); - activeSnipes.push({ - asset: asset.toUpperCase(), - side: name, - question: label, - orderId: `sim-${Date.now()}-${tokenId.slice(-6)}`, - price: config.sniperPrice, - shares: config.sniperShares, - cost, - potentialPayout: config.sniperShares, - }); - continue; - } + // Place 3 orders per side + for (let tier = 0; tier < 3; tier++) { + const price = prices[tier]; + const size = sizes[tier]; - const client = getClient(); - try { - const res = await client.createAndPostOrder( - { - tokenID: tokenId, - side: Side.BUY, - price: config.sniperPrice, - size: config.sniperShares, - }, - { tickSize, negRisk }, - OrderType.GTC, - ); - - if (res?.success) { - const cost = config.sniperPrice * config.sniperShares; - logger.trade(`SNIPER: ${asset.toUpperCase()} ${name} @ $${config.sniperPrice} × ${config.sniperShares}sh | cost $${cost.toFixed(3)} | order ${res.orderID}`); + if (config.dryRun) { + const cost = price * size; + logger.trade(`SNIPER[SIM]: ${asset.toUpperCase()} ${name} T${tier+1} @ $${price.toFixed(2)} × ${size}sh | cost $${cost.toFixed(3)}`); activeSnipes.push({ asset: asset.toUpperCase(), side: name, + tier: tier + 1, question: label, - orderId: res.orderID, - price: config.sniperPrice, - shares: config.sniperShares, + orderId: `sim-${Date.now()}-${tier}-${tokenId.slice(-6)}`, + price, + shares: size, cost, - potentialPayout: config.sniperShares, + potentialPayout: size, }); - } else { - logger.warn(`SNIPER: ${asset.toUpperCase()} ${name} order failed — ${res?.errorMsg || 'unknown'}`); + continue; + } + + const client = getClient(); + try { + const res = await client.createAndPostOrder( + { + tokenID: tokenId, + side: Side.BUY, + price: price, + size: size, + }, + { tickSize, negRisk }, + OrderType.GTC, + ); + + if (res?.success) { + const cost = price * size; + logger.trade(`SNIPER: ${asset.toUpperCase()} ${name} T${tier+1} @ $${price.toFixed(2)} × ${size}sh | cost $${cost.toFixed(3)} | order ${res.orderID}`); + activeSnipes.push({ + asset: asset.toUpperCase(), + side: name, + tier: tier + 1, + question: label, + orderId: res.orderID, + price, + shares: size, + cost, + potentialPayout: size, + }); + } else { + logger.warn(`SNIPER: ${asset.toUpperCase()} ${name} T${tier+1} failed — ${res?.errorMsg || 'unknown'}`); + } + } catch (err) { + logger.error(`SNIPER: ${asset.toUpperCase()} ${name} T${tier+1} error — ${err.message}`); } - } catch (err) { - logger.error(`SNIPER: ${asset.toUpperCase()} ${name} error — ${err.message}`); } } } diff --git a/src/sniper-tui.js b/src/sniper-tui.js index f5348bc..19c532b 100644 --- a/src/sniper-tui.js +++ b/src/sniper-tui.js @@ -159,9 +159,12 @@ process.on('SIGTERM', shutdown); // ── Start ───────────────────────────────────────────────────────────────────── -const costPerSlot = (config.sniperPrice * config.sniperShares * 2 * config.sniperAssets.length).toFixed(3); +const prices = config.sniperTierPrices; +const sizes = [Math.floor(config.sniperMaxShares * 0.20), Math.floor(config.sniperMaxShares * 0.30), Math.floor(config.sniperMaxShares * 0.50)]; +const costPerSide = (sizes[0] * prices[0]) + (sizes[1] * prices[1]) + (sizes[2] * prices[2]); +const costPerSlot = (costPerSide * 2 * config.sniperAssets.length).toFixed(3); logger.info(`SNIPER starting — ${config.dryRun ? 'SIMULATION' : 'LIVE'}`); -logger.info(`Assets: ${config.sniperAssets.join(', ').toUpperCase()} | $${config.sniperPrice} × ${config.sniperShares}sh = $${costPerSlot}/slot`); +logger.info(`Assets: ${config.sniperAssets.join(', ').toUpperCase()} | 3-tier: 3c×${sizes[0]}+2c×${sizes[1]}+1c×${sizes[2]} = $${costPerSlot}/slot`); startRefresh(); startRedeemer(); diff --git a/src/sniper.js b/src/sniper.js index e4b4378..158657e 100644 --- a/src/sniper.js +++ b/src/sniper.js @@ -102,9 +102,13 @@ process.on('SIGTERM', shutdown); // ── Start ───────────────────────────────────────────────────────────────────── -const costPerSlot = (config.sniperPrice * config.sniperShares * 2 * config.sniperAssets.length).toFixed(3); +// Calculate cost for 3-tier strategy +const prices = config.sniperTierPrices; +const sizes = [Math.floor(config.sniperMaxShares * 0.20), Math.floor(config.sniperMaxShares * 0.30), Math.floor(config.sniperMaxShares * 0.50)]; +const costPerSide = (sizes[0] * prices[0]) + (sizes[1] * prices[1]) + (sizes[2] * prices[2]); +const costPerSlot = (costPerSide * 2 * config.sniperAssets.length).toFixed(3); logger.info(`SNIPER starting — ${config.dryRun ? 'SIMULATION' : 'LIVE'}`); -logger.info(`Assets: ${config.sniperAssets.join(', ').toUpperCase()} | $${config.sniperPrice} × ${config.sniperShares}sh = $${costPerSlot}/slot`); +logger.info(`Assets: ${config.sniperAssets.join(', ').toUpperCase()} | 3-tier: 3c×${sizes[0]}+2c×${sizes[1]}+1c×${sizes[2]} = $${costPerSlot}/slot`); logSchedule(); startRedeemer();