Files
polymarket-insider-tracker/src/polymarket_insider_tracker/detector/models.py
T
Patrick SelamyandClaude Opus 4.5 96e471882a feat: implement SniperDetector with DBSCAN clustering (#16)
Add sniper cluster detection system that identifies wallets exhibiting
coordinated "sniper" behavior - consistently entering markets within
minutes of their creation.

Key features:
- SniperDetector class using DBSCAN clustering algorithm
- Tracks wallet entries across markets with timing analysis
- Feature vector: market hash, entry delta, log position size
- Identifies clusters of wallets with similar timing patterns
- Generates SniperClusterSignal for detected cluster members
- Configurable entry threshold (default 5 minutes), cluster size, DBSCAN params
- Confidence scoring based on cluster size, entry speed, market overlap

Also adds:
- SniperClusterSignal model to detector/models.py
- scikit-learn and numpy dependencies for ML clustering
- 27 comprehensive tests covering clustering logic and edge cases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:59:53 -05:00

279 lines
10 KiB
Python

"""Data models for the detector module."""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from decimal import Decimal
from polymarket_insider_tracker.ingestor.models import MarketMetadata, TradeEvent
from polymarket_insider_tracker.profiler.models import WalletProfile
@dataclass(frozen=True)
class FreshWalletSignal:
"""Signal emitted when a fresh wallet makes a suspicious trade.
This signal combines trade event data with wallet profile analysis
to produce a confidence score indicating the likelihood of suspicious
activity.
Attributes:
trade_event: The original trade event that triggered this signal.
wallet_profile: Analyzed profile of the trader's wallet.
confidence: Overall confidence score (0.0 to 1.0).
factors: Individual factor scores contributing to confidence.
timestamp: When this signal was generated.
"""
trade_event: TradeEvent
wallet_profile: WalletProfile
confidence: float
factors: dict[str, float]
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
@property
def wallet_address(self) -> str:
"""Return the wallet address from the trade event."""
return self.trade_event.wallet_address
@property
def market_id(self) -> str:
"""Return the market ID from the trade event."""
return self.trade_event.market_id
@property
def trade_size_usdc(self) -> Decimal:
"""Return the trade size in USDC (notional value)."""
return self.trade_event.notional_value
@property
def is_high_confidence(self) -> bool:
"""Return True if confidence exceeds 0.7."""
return self.confidence >= 0.7
@property
def is_very_high_confidence(self) -> bool:
"""Return True if confidence exceeds 0.85."""
return self.confidence >= 0.85
def to_dict(self) -> dict[str, object]:
"""Serialize to dictionary for Redis stream publishing."""
return {
"wallet_address": self.wallet_address,
"market_id": self.market_id,
"trade_id": self.trade_event.trade_id,
"trade_size": str(self.trade_size_usdc),
"trade_side": self.trade_event.side,
"trade_price": str(self.trade_event.price),
"wallet_nonce": self.wallet_profile.nonce,
"wallet_age_hours": self.wallet_profile.age_hours,
"wallet_is_fresh": self.wallet_profile.is_fresh,
"confidence": self.confidence,
"factors": self.factors,
"timestamp": self.timestamp.isoformat(),
}
@dataclass(frozen=True)
class SizeAnomalySignal:
"""Signal emitted when a trade has unusually large position size.
This signal is generated when a trade's size significantly impacts
the market volume or order book depth, indicating potential informed
trading activity.
Attributes:
trade_event: The original trade event that triggered this signal.
market_metadata: Metadata about the market being traded.
volume_impact: Trade size as fraction of 24h volume (0.0 if unknown).
book_impact: Trade size as fraction of order book depth (0.0 if unknown).
is_niche_market: Whether the market is considered niche/low-volume.
confidence: Overall confidence score (0.0 to 1.0).
factors: Individual factor scores contributing to confidence.
timestamp: When this signal was generated.
"""
trade_event: TradeEvent
market_metadata: MarketMetadata
volume_impact: float
book_impact: float
is_niche_market: bool
confidence: float
factors: dict[str, float]
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
@property
def wallet_address(self) -> str:
"""Return the wallet address from the trade event."""
return self.trade_event.wallet_address
@property
def market_id(self) -> str:
"""Return the market ID from the trade event."""
return self.trade_event.market_id
@property
def trade_size_usdc(self) -> Decimal:
"""Return the trade size in USDC (notional value)."""
return self.trade_event.notional_value
@property
def is_high_confidence(self) -> bool:
"""Return True if confidence exceeds 0.7."""
return self.confidence >= 0.7
@property
def is_very_high_confidence(self) -> bool:
"""Return True if confidence exceeds 0.85."""
return self.confidence >= 0.85
def to_dict(self) -> dict[str, object]:
"""Serialize to dictionary for Redis stream publishing."""
return {
"wallet_address": self.wallet_address,
"market_id": self.market_id,
"trade_id": self.trade_event.trade_id,
"trade_size": str(self.trade_size_usdc),
"trade_side": self.trade_event.side,
"trade_price": str(self.trade_event.price),
"market_category": self.market_metadata.category,
"volume_impact": self.volume_impact,
"book_impact": self.book_impact,
"is_niche_market": self.is_niche_market,
"confidence": self.confidence,
"factors": self.factors,
"timestamp": self.timestamp.isoformat(),
}
@dataclass(frozen=True)
class SniperClusterSignal:
"""Signal emitted when a wallet is identified as part of a sniper cluster.
Sniper clusters are groups of wallets that consistently enter markets
within minutes of their creation, suggesting coordinated insider activity.
Attributes:
wallet_address: The wallet identified as a sniper.
cluster_id: Unique identifier for this cluster.
cluster_size: Number of wallets in the cluster.
avg_entry_delta_seconds: Average time (seconds) from market creation to entry.
markets_in_common: Number of markets where cluster members overlap.
confidence: Confidence score (0.0 to 1.0) based on clustering strength.
timestamp: When this signal was generated.
"""
wallet_address: str
cluster_id: str
cluster_size: int
avg_entry_delta_seconds: float
markets_in_common: int
confidence: float
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
@property
def is_high_confidence(self) -> bool:
"""Return True if confidence exceeds 0.7."""
return self.confidence >= 0.7
@property
def is_very_high_confidence(self) -> bool:
"""Return True if confidence exceeds 0.85."""
return self.confidence >= 0.85
def to_dict(self) -> dict[str, object]:
"""Serialize to dictionary for Redis stream publishing."""
return {
"wallet_address": self.wallet_address,
"cluster_id": self.cluster_id,
"cluster_size": self.cluster_size,
"avg_entry_delta_seconds": self.avg_entry_delta_seconds,
"markets_in_common": self.markets_in_common,
"confidence": self.confidence,
"timestamp": self.timestamp.isoformat(),
}
@dataclass(frozen=True)
class RiskAssessment:
"""Combined risk assessment aggregating all signal types.
This represents the final scoring output that determines whether
a trade should trigger an alert, combining signals from multiple
detectors with configurable weights.
Attributes:
trade_event: The original trade event being assessed.
wallet_address: The trader's wallet address.
market_id: The market condition ID.
fresh_wallet_signal: Signal from fresh wallet detector, if triggered.
size_anomaly_signal: Signal from size anomaly detector, if triggered.
signals_triggered: Count of how many signal types fired.
weighted_score: Final weighted combination of all signals (0.0 to 1.0).
should_alert: Whether this assessment meets alert threshold.
assessment_id: Unique identifier for this assessment.
timestamp: When this assessment was generated.
"""
trade_event: TradeEvent
wallet_address: str
market_id: str
# Individual signals (None if not triggered)
fresh_wallet_signal: FreshWalletSignal | None
size_anomaly_signal: SizeAnomalySignal | None
# Combined scoring
signals_triggered: int
weighted_score: float
should_alert: bool
# Metadata
assessment_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
@property
def is_high_risk(self) -> bool:
"""Return True if weighted score exceeds 0.7."""
return self.weighted_score >= 0.7
@property
def is_very_high_risk(self) -> bool:
"""Return True if weighted score exceeds 0.85."""
return self.weighted_score >= 0.85
@property
def trade_size_usdc(self) -> Decimal:
"""Return the trade size in USDC (notional value)."""
return self.trade_event.notional_value
def to_dict(self) -> dict[str, object]:
"""Serialize to dictionary for Redis stream publishing."""
return {
"assessment_id": self.assessment_id,
"wallet_address": self.wallet_address,
"market_id": self.market_id,
"trade_id": self.trade_event.trade_id,
"trade_size": str(self.trade_size_usdc),
"trade_side": self.trade_event.side,
"trade_price": str(self.trade_event.price),
"signals_triggered": self.signals_triggered,
"weighted_score": self.weighted_score,
"should_alert": self.should_alert,
"has_fresh_wallet_signal": self.fresh_wallet_signal is not None,
"has_size_anomaly_signal": self.size_anomaly_signal is not None,
"fresh_wallet_confidence": (
self.fresh_wallet_signal.confidence
if self.fresh_wallet_signal
else None
),
"size_anomaly_confidence": (
self.size_anomaly_signal.confidence
if self.size_anomaly_signal
else None
),
"timestamp": self.timestamp.isoformat(),
}