feat: implement 3-tier sniper strategy with weighted sizing
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
00235dc850
commit
c524ed92ef
+11
-7
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
+10
-4
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -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();
|
||||
|
||||
+6
-2
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user