Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
},
"symbols": {
"ASTERUSDT": {
"trade_side": "OPPOSITE",
"longVolumeThresholdUSDT": 1000,
"shortVolumeThresholdUSDT": 2500,
"tradeSize": 0.69,
Expand Down
40 changes: 39 additions & 1 deletion src/components/SymbolConfigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig
priceOffsetBps: 5, // 5 basis points offset for limit orders
maxSlippageBps: 50, // 50 basis points max slippage
orderType: 'LIMIT' as const,
trade_side: 'OPPOSITE' as const, // Default to contrarian strategy
vwapProtection: false, // VWAP protection disabled by default
vwapTimeframe: '1m', // Default to 1 minute timeframe
vwapLookback: 100, // Default to 100 candles
Expand Down Expand Up @@ -1065,6 +1066,43 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig
</p>
</div>

{/* Trade Direction Strategy */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Trade Direction
</Label>
<Select
value={config.symbols[selectedSymbol].trade_side || 'OPPOSITE'}
onValueChange={(value: 'OPPOSITE' | 'SAME') =>
handleSymbolChange(selectedSymbol, 'trade_side', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="OPPOSITE">
<div className="flex flex-col items-start">
<span>Opposite (Contrarian)</span>
<span className="text-xs text-muted-foreground">Trade against liquidations</span>
</div>
</SelectItem>
<SelectItem value="SAME">
<div className="flex flex-col items-start">
<span>Same (Momentum)</span>
<span className="text-xs text-muted-foreground">Trade with liquidations</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{config.symbols[selectedSymbol].trade_side === 'SAME'
? 'Buy on buy liquidations, sell on sell liquidations (momentum strategy)'
: 'Buy on sell liquidations, sell on buy liquidations (contrarian strategy - default)'}
</p>
</div>

{/* VWAP Protection Settings */}
<div className="col-span-2">
<Separator className="my-4" />
Expand Down Expand Up @@ -1251,4 +1289,4 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig
</div>
</div>
);
}
}
95 changes: 72 additions & 23 deletions src/lib/bot/hunter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,11 +436,22 @@ export class Hunter extends EventEmitter {

// Check if we should use threshold system or instant trigger
if (useThresholdSystem && thresholdStatus) {
// NEW THRESHOLD SYSTEM - Cumulative volume in 60-second window
// SELL liquidation means longs are getting liquidated, we might want to BUY
// BUY liquidation means shorts are getting liquidated, we might want to SELL
const isLongOpportunity = liquidation.side === 'SELL';
const isShortOpportunity = liquidation.side === 'BUY';
// * NEW THRESHOLD SYSTEM - Cumulative volume in 60-second window
// * Determine opportunity direction based on trade_side parameter
const tradeDirection = symbolConfig.trade_side || 'OPPOSITE';

let isLongOpportunity: boolean;
let isShortOpportunity: boolean;

if (tradeDirection === 'OPPOSITE') {
// * Contrarian: SELL liquidation → BUY opportunity, BUY liquidation → SELL opportunity
isLongOpportunity = liquidation.side === 'SELL';
isShortOpportunity = liquidation.side === 'BUY';
} else {
// * Momentum: SELL liquidation → SELL opportunity, BUY liquidation → BUY opportunity
isLongOpportunity = liquidation.side === 'BUY';
isShortOpportunity = liquidation.side === 'SELL';
}

let shouldTrade = false;
let tradeSide: 'BUY' | 'SELL' | null = null;
Expand Down Expand Up @@ -490,17 +501,26 @@ export class Hunter extends EventEmitter {
await this.analyzeAndTrade(liquidation, symbolConfig, tradeSide);
}
} else {
// ORIGINAL INSTANT TRIGGER SYSTEM
// Check direction-specific volume thresholds
// SELL liquidation means longs are getting liquidated, we might want to BUY
// BUY liquidation means shorts are getting liquidated, we might want to SELL
const thresholdToCheck = liquidation.side === 'SELL'
? (symbolConfig.longVolumeThresholdUSDT ?? symbolConfig.volumeThresholdUSDT ?? 0)
: (symbolConfig.shortVolumeThresholdUSDT ?? symbolConfig.volumeThresholdUSDT ?? 0);
// * ORIGINAL INSTANT TRIGGER SYSTEM - Check direction-specific volume thresholds
// * Determine threshold based on trade_side parameter
const tradeDirection = symbolConfig.trade_side || 'OPPOSITE';

let thresholdToCheck: number;
if (tradeDirection === 'OPPOSITE') {
// * Contrarian: Use thresholds based on liquidation side
thresholdToCheck = liquidation.side === 'SELL'
? (symbolConfig.longVolumeThresholdUSDT ?? symbolConfig.volumeThresholdUSDT ?? 0)
: (symbolConfig.shortVolumeThresholdUSDT ?? symbolConfig.volumeThresholdUSDT ?? 0);
} else {
// * Momentum: Use thresholds based on opposite of liquidation side
thresholdToCheck = liquidation.side === 'BUY'
? (symbolConfig.longVolumeThresholdUSDT ?? symbolConfig.volumeThresholdUSDT ?? 0)
: (symbolConfig.shortVolumeThresholdUSDT ?? symbolConfig.volumeThresholdUSDT ?? 0);
}

if (volumeUSDT < thresholdToCheck) return; // Too small

console.log(`Hunter: Liquidation detected - ${liquidation.symbol} ${liquidation.side} ${volumeUSDT.toFixed(2)} USDT`);
console.log(`Hunter: [${tradeDirection}] Liquidation detected - ${liquidation.symbol} ${liquidation.side} ${volumeUSDT.toFixed(2)} USDT`);

// Analyze and trade with instant trigger
await this.analyzeAndTrade(liquidation, symbolConfig);
Expand All @@ -516,11 +536,24 @@ export class Hunter extends EventEmitter {

const markPrice = parseFloat(markPriceData.markPrice);

// Simple analysis: If SELL liquidation and price is > 0.99 * mark, buy
// If BUY liquidation, sell
// Determine trade direction based on trade_side parameter
// * OPPOSITE (default): SELL liquidation → BUY, BUY liquidation → SELL (contrarian strategy)
// * SAME: SELL liquidation → SELL, BUY liquidation → BUY (momentum strategy)
const tradeSide = symbolConfig.trade_side || 'OPPOSITE';
const priceRatio = liquidation.price / markPrice;
const triggerBuy = liquidation.side === 'SELL' && priceRatio < 1.01; // 1% below
const triggerSell = liquidation.side === 'BUY' && priceRatio > 0.99; // 1% above

let triggerBuy: boolean;
let triggerSell: boolean;

if (tradeSide === 'OPPOSITE') {
// * Contrarian: Trade opposite to liquidation direction
triggerBuy = liquidation.side === 'SELL' && priceRatio < 1.01; // SELL liquidation → BUY
triggerSell = liquidation.side === 'BUY' && priceRatio > 0.99; // BUY liquidation → SELL
} else {
// * Momentum: Trade in same direction as liquidation
triggerBuy = liquidation.side === 'BUY' && priceRatio > 0.99; // BUY liquidation → BUY
triggerSell = liquidation.side === 'SELL' && priceRatio < 1.01; // SELL liquidation → SELL
}

// Check VWAP protection if enabled
if (symbolConfig.vwapProtection) {
Expand Down Expand Up @@ -619,32 +652,48 @@ export class Hunter extends EventEmitter {
if (triggerBuy) {
const volumeUSDT = liquidation.qty * liquidation.price;

// * Generate reason based on trade side mode
let reason: string;
if (tradeSide === 'OPPOSITE') {
reason = `SELL liquidation at ${((1 - priceRatio) * 100).toFixed(2)}% below mark price (contrarian)`;
} else {
reason = `BUY liquidation at ${((priceRatio - 1) * 100).toFixed(2)}% above mark price (momentum)`;
}

// Emit trade opportunity
this.emit('tradeOpportunity', {
symbol: liquidation.symbol,
side: 'BUY',
reason: `SELL liquidation at ${((1 - priceRatio) * 100).toFixed(2)}% below mark price`,
reason: reason,
liquidationVolume: volumeUSDT,
priceImpact: (1 - priceRatio) * 100,
priceImpact: Math.abs((priceRatio - 1) * 100),
confidence: Math.min(95, 50 + (volumeUSDT / 1000) * 10) // Higher confidence for larger volumes
});

console.log(`Hunter: Triggering BUY for ${liquidation.symbol} at ${liquidation.price}`);
console.log(`Hunter: [${tradeSide}] Triggering BUY for ${liquidation.symbol} at ${liquidation.price}`);
await this.placeTrade(liquidation.symbol, 'BUY', symbolConfig, liquidation.price);
} else if (triggerSell) {
const volumeUSDT = liquidation.qty * liquidation.price;

// * Generate reason based on trade side mode
let reason: string;
if (tradeSide === 'OPPOSITE') {
reason = `BUY liquidation at ${((priceRatio - 1) * 100).toFixed(2)}% above mark price (contrarian)`;
} else {
reason = `SELL liquidation at ${((1 - priceRatio) * 100).toFixed(2)}% below mark price (momentum)`;
}

// Emit trade opportunity
this.emit('tradeOpportunity', {
symbol: liquidation.symbol,
side: 'SELL',
reason: `BUY liquidation at ${((priceRatio - 1) * 100).toFixed(2)}% above mark price`,
reason: reason,
liquidationVolume: volumeUSDT,
priceImpact: (priceRatio - 1) * 100,
priceImpact: Math.abs((priceRatio - 1) * 100),
confidence: Math.min(95, 50 + (volumeUSDT / 1000) * 10)
});

console.log(`Hunter: Triggering SELL for ${liquidation.symbol} at ${liquidation.price}`);
console.log(`Hunter: [${tradeSide}] Triggering SELL for ${liquidation.symbol} at ${liquidation.price}`);
await this.placeTrade(liquidation.symbol, 'SELL', symbolConfig, liquidation.price);
}
} catch (error) {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export const symbolConfigSchema = z.object({
vwapTimeframe: z.string().optional(),
vwapLookback: z.number().min(10).max(500).optional(),

// Trade direction settings
trade_side: z.enum(['OPPOSITE', 'SAME']).optional(),

// Threshold system settings
useThreshold: z.boolean().optional(),
}).refine(data => {
Expand Down
18 changes: 13 additions & 5 deletions src/lib/services/thresholdMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,18 @@ export class ThresholdMonitor extends EventEmitter {

const now = Date.now();

// Determine which side this liquidation affects
// SELL liquidation means longs are getting liquidated, we might want to BUY (long)
// BUY liquidation means shorts are getting liquidated, we might want to SELL (short)
const isLongOpportunity = liquidation.side === 'SELL';
// * Determine which side this liquidation affects based on trade_side parameter
const symbolConfigForTradeSide = this.config.symbols[liquidation.symbol];
const tradeSide = symbolConfigForTradeSide?.trade_side || 'OPPOSITE';

let isLongOpportunity: boolean;
if (tradeSide === 'OPPOSITE') {
// * Contrarian: SELL liquidation → BUY opportunity, BUY liquidation → SELL opportunity
isLongOpportunity = liquidation.side === 'SELL';
} else {
// * Momentum: SELL liquidation → SELL opportunity, BUY liquidation → BUY opportunity
isLongOpportunity = liquidation.side === 'BUY';
}

if (isLongOpportunity) {
status.recentLiquidations.long.push(liquidation);
Expand Down Expand Up @@ -370,4 +378,4 @@ export class ThresholdMonitor extends EventEmitter {
}

// Export singleton instance with 30-second cooldown
export const thresholdMonitor = new ThresholdMonitor(undefined, 60 * 1000, 30 * 1000);
export const thresholdMonitor = new ThresholdMonitor(undefined, 60 * 1000, 30 * 1000);
3 changes: 3 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export interface SymbolConfig {
vwapTimeframe?: string; // Timeframe for VWAP calculation: 1m, 5m, 15m, 30m, 1h (default: '1m')
vwapLookback?: number; // Number of candles to use for VWAP calculation (default: 100)

// Trade direction settings
trade_side?: 'OPPOSITE' | 'SAME'; // Trade direction relative to liquidation: OPPOSITE (default) or SAME

// Threshold system settings (60-second rolling window)
useThreshold?: boolean; // Enable threshold-based triggering for this symbol (default: false)
thresholdTimeWindow?: number; // Time window in ms for volume accumulation (default: 60000)
Expand Down
Loading