78243a6fb8
Add FreshWalletDetector class that identifies trades from fresh wallets and generates alert signals with confidence scores. Features: - FreshWalletSignal dataclass with trade/wallet data and confidence - Configurable thresholds for nonce, age, and minimum trade size - Confidence scoring based on wallet freshness and trade characteristics - Batch analysis support with parallel processing - Integration with WalletAnalyzer from profiler module Confidence scoring: - Base: 0.5 (fresh wallet detected) - +0.2 if nonce == 0 (brand new) - +0.1 if age < 2 hours (very young) - +0.1 if trade size > $10,000 (large trade) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
609 lines
21 KiB
Python
609 lines
21 KiB
Python
"""Tests for the fresh wallet detector."""
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
from decimal import Decimal
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from polymarket_insider_tracker.detector.fresh_wallet import (
|
|
BASE_CONFIDENCE,
|
|
BRAND_NEW_BONUS,
|
|
DEFAULT_MAX_AGE_HOURS,
|
|
DEFAULT_MAX_NONCE,
|
|
DEFAULT_MIN_TRADE_SIZE,
|
|
LARGE_TRADE_BONUS,
|
|
VERY_YOUNG_BONUS,
|
|
FreshWalletDetector,
|
|
)
|
|
from polymarket_insider_tracker.detector.models import FreshWalletSignal
|
|
from polymarket_insider_tracker.ingestor.models import TradeEvent
|
|
from polymarket_insider_tracker.profiler.models import WalletProfile
|
|
|
|
|
|
# Test fixtures
|
|
@pytest.fixture
|
|
def mock_wallet_analyzer():
|
|
"""Create a mock WalletAnalyzer."""
|
|
return AsyncMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def detector(mock_wallet_analyzer):
|
|
"""Create a FreshWalletDetector with mocked analyzer."""
|
|
return FreshWalletDetector(mock_wallet_analyzer)
|
|
|
|
|
|
def create_trade_event(
|
|
*,
|
|
wallet_address: str = "0x1234567890123456789012345678901234567890",
|
|
market_id: str = "market123",
|
|
trade_id: str = "trade123",
|
|
price: Decimal = Decimal("0.5"),
|
|
size: Decimal = Decimal("2000"),
|
|
side: str = "BUY",
|
|
) -> TradeEvent:
|
|
"""Create a TradeEvent for testing."""
|
|
return TradeEvent(
|
|
market_id=market_id,
|
|
trade_id=trade_id,
|
|
wallet_address=wallet_address,
|
|
side=side,
|
|
outcome="Yes",
|
|
outcome_index=0,
|
|
price=price,
|
|
size=size,
|
|
timestamp=datetime.now(UTC),
|
|
asset_id="asset123",
|
|
)
|
|
|
|
|
|
def create_wallet_profile(
|
|
*,
|
|
address: str = "0x1234567890123456789012345678901234567890",
|
|
nonce: int = 2,
|
|
age_hours: float | None = 24.0,
|
|
is_fresh: bool = True,
|
|
) -> WalletProfile:
|
|
"""Create a WalletProfile for testing."""
|
|
first_seen = None
|
|
if age_hours is not None:
|
|
first_seen = datetime.now(UTC) - timedelta(hours=age_hours)
|
|
|
|
return WalletProfile(
|
|
address=address.lower(),
|
|
nonce=nonce,
|
|
first_seen=first_seen,
|
|
age_hours=age_hours,
|
|
is_fresh=is_fresh,
|
|
total_tx_count=nonce,
|
|
matic_balance=Decimal("1000000000000000000"), # 1 MATIC
|
|
usdc_balance=Decimal("1000000000"), # 1000 USDC
|
|
fresh_threshold=5,
|
|
)
|
|
|
|
|
|
# === Test FreshWalletSignal Model ===
|
|
|
|
|
|
class TestFreshWalletSignal:
|
|
"""Tests for FreshWalletSignal dataclass."""
|
|
|
|
def test_wallet_address_property(self):
|
|
"""Test wallet_address property returns trade wallet."""
|
|
trade = create_trade_event(wallet_address="0xabc123")
|
|
profile = create_wallet_profile(address="0xabc123")
|
|
signal = FreshWalletSignal(
|
|
trade_event=trade,
|
|
wallet_profile=profile,
|
|
confidence=0.7,
|
|
factors={"base": 0.5},
|
|
)
|
|
assert signal.wallet_address == "0xabc123"
|
|
|
|
def test_market_id_property(self):
|
|
"""Test market_id property returns trade market."""
|
|
trade = create_trade_event(market_id="market456")
|
|
profile = create_wallet_profile()
|
|
signal = FreshWalletSignal(
|
|
trade_event=trade,
|
|
wallet_profile=profile,
|
|
confidence=0.7,
|
|
factors={"base": 0.5},
|
|
)
|
|
assert signal.market_id == "market456"
|
|
|
|
def test_trade_size_usdc_property(self):
|
|
"""Test trade_size_usdc returns notional value."""
|
|
trade = create_trade_event(price=Decimal("0.5"), size=Decimal("2000"))
|
|
profile = create_wallet_profile()
|
|
signal = FreshWalletSignal(
|
|
trade_event=trade,
|
|
wallet_profile=profile,
|
|
confidence=0.7,
|
|
factors={"base": 0.5},
|
|
)
|
|
assert signal.trade_size_usdc == Decimal("1000")
|
|
|
|
def test_is_high_confidence(self):
|
|
"""Test is_high_confidence threshold."""
|
|
trade = create_trade_event()
|
|
profile = create_wallet_profile()
|
|
|
|
# At threshold
|
|
signal = FreshWalletSignal(
|
|
trade_event=trade,
|
|
wallet_profile=profile,
|
|
confidence=0.7,
|
|
factors={},
|
|
)
|
|
assert signal.is_high_confidence is True
|
|
|
|
# Below threshold
|
|
signal_low = FreshWalletSignal(
|
|
trade_event=trade,
|
|
wallet_profile=profile,
|
|
confidence=0.69,
|
|
factors={},
|
|
)
|
|
assert signal_low.is_high_confidence is False
|
|
|
|
def test_is_very_high_confidence(self):
|
|
"""Test is_very_high_confidence threshold."""
|
|
trade = create_trade_event()
|
|
profile = create_wallet_profile()
|
|
|
|
# At threshold
|
|
signal = FreshWalletSignal(
|
|
trade_event=trade,
|
|
wallet_profile=profile,
|
|
confidence=0.85,
|
|
factors={},
|
|
)
|
|
assert signal.is_very_high_confidence is True
|
|
|
|
# Below threshold
|
|
signal_low = FreshWalletSignal(
|
|
trade_event=trade,
|
|
wallet_profile=profile,
|
|
confidence=0.84,
|
|
factors={},
|
|
)
|
|
assert signal_low.is_very_high_confidence is False
|
|
|
|
def test_to_dict(self):
|
|
"""Test serialization to dictionary."""
|
|
trade = create_trade_event(
|
|
wallet_address="0xtest",
|
|
market_id="market1",
|
|
trade_id="tx1",
|
|
price=Decimal("0.6"),
|
|
size=Decimal("5000"),
|
|
side="BUY",
|
|
)
|
|
profile = create_wallet_profile(address="0xtest", nonce=0, age_hours=1.0)
|
|
signal = FreshWalletSignal(
|
|
trade_event=trade,
|
|
wallet_profile=profile,
|
|
confidence=0.8,
|
|
factors={"base": 0.5, "brand_new": 0.2},
|
|
)
|
|
|
|
result = signal.to_dict()
|
|
|
|
assert result["wallet_address"] == "0xtest"
|
|
assert result["market_id"] == "market1"
|
|
assert result["trade_id"] == "tx1"
|
|
assert result["trade_size"] == "3000.0"
|
|
assert result["trade_side"] == "BUY"
|
|
assert result["trade_price"] == "0.6"
|
|
assert result["wallet_nonce"] == 0
|
|
assert result["wallet_age_hours"] == 1.0
|
|
assert result["wallet_is_fresh"] is True
|
|
assert result["confidence"] == 0.8
|
|
assert result["factors"] == {"base": 0.5, "brand_new": 0.2}
|
|
assert "timestamp" in result
|
|
|
|
|
|
# === Test FreshWalletDetector Initialization ===
|
|
|
|
|
|
class TestFreshWalletDetectorInit:
|
|
"""Tests for FreshWalletDetector initialization."""
|
|
|
|
def test_default_config(self, mock_wallet_analyzer):
|
|
"""Test detector initializes with default config."""
|
|
detector = FreshWalletDetector(mock_wallet_analyzer)
|
|
assert detector._min_trade_size == DEFAULT_MIN_TRADE_SIZE
|
|
assert detector._max_nonce == DEFAULT_MAX_NONCE
|
|
assert detector._max_age_hours == DEFAULT_MAX_AGE_HOURS
|
|
|
|
def test_custom_config(self, mock_wallet_analyzer):
|
|
"""Test detector accepts custom config."""
|
|
detector = FreshWalletDetector(
|
|
mock_wallet_analyzer,
|
|
min_trade_size=Decimal("5000"),
|
|
max_nonce=10,
|
|
max_age_hours=72.0,
|
|
)
|
|
assert detector._min_trade_size == Decimal("5000")
|
|
assert detector._max_nonce == 10
|
|
assert detector._max_age_hours == 72.0
|
|
|
|
|
|
# === Test FreshWalletDetector.analyze ===
|
|
|
|
|
|
class TestFreshWalletDetectorAnalyze:
|
|
"""Tests for FreshWalletDetector.analyze method."""
|
|
|
|
async def test_filters_small_trades(self, detector, mock_wallet_analyzer):
|
|
"""Test that trades below minimum size are filtered out."""
|
|
trade = create_trade_event(
|
|
price=Decimal("0.1"),
|
|
size=Decimal("100"), # notional = $10
|
|
)
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
assert result is None
|
|
mock_wallet_analyzer.analyze.assert_not_called()
|
|
|
|
async def test_detects_fresh_wallet(self, detector, mock_wallet_analyzer):
|
|
"""Test detection of fresh wallet trade."""
|
|
trade = create_trade_event(
|
|
price=Decimal("0.5"),
|
|
size=Decimal("2000"), # notional = $1000
|
|
)
|
|
profile = create_wallet_profile(nonce=2, age_hours=24.0, is_fresh=True)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
assert result is not None
|
|
assert isinstance(result, FreshWalletSignal)
|
|
assert result.trade_event == trade
|
|
assert result.wallet_profile == profile
|
|
assert result.confidence >= BASE_CONFIDENCE
|
|
|
|
async def test_filters_non_fresh_wallet(self, detector, mock_wallet_analyzer):
|
|
"""Test that non-fresh wallets are filtered out."""
|
|
trade = create_trade_event(
|
|
price=Decimal("0.5"),
|
|
size=Decimal("4000"), # notional = $2000
|
|
)
|
|
profile = create_wallet_profile(nonce=10, age_hours=100.0, is_fresh=False)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
assert result is None
|
|
|
|
async def test_handles_analyzer_error(self, detector, mock_wallet_analyzer):
|
|
"""Test graceful handling of analyzer errors."""
|
|
trade = create_trade_event()
|
|
mock_wallet_analyzer.analyze.side_effect = Exception("RPC error")
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
assert result is None
|
|
|
|
async def test_wallet_at_nonce_threshold(self, detector, mock_wallet_analyzer):
|
|
"""Test wallet exactly at max_nonce threshold."""
|
|
trade = create_trade_event(size=Decimal("3000"))
|
|
profile = create_wallet_profile(nonce=5, age_hours=24.0) # At threshold
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
assert result is not None
|
|
|
|
async def test_wallet_above_nonce_threshold(self, detector, mock_wallet_analyzer):
|
|
"""Test wallet above max_nonce threshold."""
|
|
trade = create_trade_event(size=Decimal("3000"))
|
|
profile = create_wallet_profile(nonce=6, age_hours=24.0) # Above threshold
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
assert result is None
|
|
|
|
async def test_wallet_at_age_threshold(self, detector, mock_wallet_analyzer):
|
|
"""Test wallet exactly at max_age_hours threshold."""
|
|
trade = create_trade_event(size=Decimal("3000"))
|
|
profile = create_wallet_profile(nonce=2, age_hours=48.0) # At threshold
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
assert result is not None
|
|
|
|
async def test_wallet_above_age_threshold(self, detector, mock_wallet_analyzer):
|
|
"""Test wallet above max_age_hours threshold."""
|
|
trade = create_trade_event(size=Decimal("3000"))
|
|
profile = create_wallet_profile(nonce=2, age_hours=49.0) # Above threshold
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
assert result is None
|
|
|
|
async def test_wallet_with_unknown_age(self, detector, mock_wallet_analyzer):
|
|
"""Test wallet with unknown age (None)."""
|
|
trade = create_trade_event(size=Decimal("3000"))
|
|
profile = create_wallet_profile(nonce=2, age_hours=None)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
# Should pass since age is unknown
|
|
assert result is not None
|
|
|
|
|
|
# === Test Confidence Scoring ===
|
|
|
|
|
|
class TestConfidenceScoring:
|
|
"""Tests for confidence score calculation."""
|
|
|
|
def test_base_confidence_only(self, detector):
|
|
"""Test base confidence for fresh wallet."""
|
|
profile = create_wallet_profile(nonce=3, age_hours=24.0)
|
|
trade = create_trade_event(size=Decimal("2000")) # $1000 notional
|
|
|
|
confidence, factors = detector.calculate_confidence(profile, trade)
|
|
|
|
assert confidence == BASE_CONFIDENCE
|
|
assert factors == {"base": BASE_CONFIDENCE}
|
|
|
|
def test_brand_new_bonus(self, detector):
|
|
"""Test bonus for brand new wallet (nonce=0)."""
|
|
profile = create_wallet_profile(nonce=0, age_hours=24.0)
|
|
trade = create_trade_event(size=Decimal("2000"))
|
|
|
|
confidence, factors = detector.calculate_confidence(profile, trade)
|
|
|
|
expected = BASE_CONFIDENCE + BRAND_NEW_BONUS
|
|
assert confidence == expected
|
|
assert factors["brand_new"] == BRAND_NEW_BONUS
|
|
|
|
def test_very_young_bonus(self, detector):
|
|
"""Test bonus for very young wallet (age < 2 hours)."""
|
|
profile = create_wallet_profile(nonce=3, age_hours=1.5)
|
|
trade = create_trade_event(size=Decimal("2000"))
|
|
|
|
confidence, factors = detector.calculate_confidence(profile, trade)
|
|
|
|
expected = BASE_CONFIDENCE + VERY_YOUNG_BONUS
|
|
assert confidence == expected
|
|
assert factors["very_young"] == VERY_YOUNG_BONUS
|
|
|
|
def test_large_trade_bonus(self, detector):
|
|
"""Test bonus for large trade (> $10,000)."""
|
|
profile = create_wallet_profile(nonce=3, age_hours=24.0)
|
|
trade = create_trade_event(
|
|
price=Decimal("0.5"),
|
|
size=Decimal("25000"), # $12,500 notional
|
|
)
|
|
|
|
confidence, factors = detector.calculate_confidence(profile, trade)
|
|
|
|
expected = BASE_CONFIDENCE + LARGE_TRADE_BONUS
|
|
assert confidence == expected
|
|
assert factors["large_trade"] == LARGE_TRADE_BONUS
|
|
|
|
def test_all_bonuses_combined(self, detector):
|
|
"""Test all bonuses stacking."""
|
|
profile = create_wallet_profile(nonce=0, age_hours=0.5)
|
|
trade = create_trade_event(
|
|
price=Decimal("0.5"),
|
|
size=Decimal("30000"), # $15,000 notional
|
|
)
|
|
|
|
confidence, factors = detector.calculate_confidence(profile, trade)
|
|
|
|
expected = BASE_CONFIDENCE + BRAND_NEW_BONUS + VERY_YOUNG_BONUS + LARGE_TRADE_BONUS
|
|
assert confidence == expected
|
|
assert factors["brand_new"] == BRAND_NEW_BONUS
|
|
assert factors["very_young"] == VERY_YOUNG_BONUS
|
|
assert factors["large_trade"] == LARGE_TRADE_BONUS
|
|
|
|
def test_confidence_clamped_to_max(self, detector):
|
|
"""Test confidence is clamped to 1.0 max."""
|
|
# Create scenario where total would exceed 1.0
|
|
# BASE=0.5 + BRAND_NEW=0.2 + VERY_YOUNG=0.1 + LARGE_TRADE=0.1 = 0.9
|
|
# This is less than 1.0, so let's verify clamping works
|
|
profile = create_wallet_profile(nonce=0, age_hours=0.5)
|
|
trade = create_trade_event(size=Decimal("30000"))
|
|
|
|
confidence, _ = detector.calculate_confidence(profile, trade)
|
|
|
|
assert confidence <= 1.0
|
|
|
|
def test_no_bonus_at_age_boundary(self, detector):
|
|
"""Test no bonus when age exactly at 2 hours."""
|
|
profile = create_wallet_profile(nonce=3, age_hours=2.0)
|
|
trade = create_trade_event(size=Decimal("2000"))
|
|
|
|
confidence, factors = detector.calculate_confidence(profile, trade)
|
|
|
|
assert "very_young" not in factors
|
|
assert confidence == BASE_CONFIDENCE
|
|
|
|
def test_no_bonus_at_trade_size_boundary(self, detector):
|
|
"""Test no bonus when trade exactly at $10,000."""
|
|
profile = create_wallet_profile(nonce=3, age_hours=24.0)
|
|
trade = create_trade_event(
|
|
price=Decimal("0.5"),
|
|
size=Decimal("20000"), # $10,000 notional exactly
|
|
)
|
|
|
|
confidence, factors = detector.calculate_confidence(profile, trade)
|
|
|
|
# Exactly at threshold should NOT get bonus
|
|
assert "large_trade" not in factors
|
|
assert confidence == BASE_CONFIDENCE
|
|
|
|
def test_unknown_age_no_young_bonus(self, detector):
|
|
"""Test no young bonus when age is None."""
|
|
profile = create_wallet_profile(nonce=3, age_hours=None)
|
|
trade = create_trade_event(size=Decimal("2000"))
|
|
|
|
confidence, factors = detector.calculate_confidence(profile, trade)
|
|
|
|
assert "very_young" not in factors
|
|
assert confidence == BASE_CONFIDENCE
|
|
|
|
|
|
# === Test Batch Analysis ===
|
|
|
|
|
|
class TestBatchAnalysis:
|
|
"""Tests for batch trade analysis."""
|
|
|
|
async def test_analyze_batch_success(self, detector, mock_wallet_analyzer):
|
|
"""Test analyzing multiple trades."""
|
|
trades = [
|
|
create_trade_event(trade_id="trade1", size=Decimal("3000")),
|
|
create_trade_event(trade_id="trade2", size=Decimal("4000")),
|
|
]
|
|
profile = create_wallet_profile(nonce=2, age_hours=24.0)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
results = await detector.analyze_batch(trades)
|
|
|
|
assert len(results) == 2
|
|
assert all(isinstance(r, FreshWalletSignal) for r in results)
|
|
|
|
async def test_analyze_batch_filters_small(self, detector, mock_wallet_analyzer):
|
|
"""Test batch analysis filters small trades."""
|
|
trades = [
|
|
create_trade_event(trade_id="trade1", size=Decimal("3000")), # Above threshold
|
|
create_trade_event(trade_id="trade2", size=Decimal("100")), # Below threshold
|
|
]
|
|
profile = create_wallet_profile(nonce=2, age_hours=24.0)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
results = await detector.analyze_batch(trades)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].trade_event.trade_id == "trade1"
|
|
|
|
async def test_analyze_batch_handles_errors(self, detector, mock_wallet_analyzer):
|
|
"""Test batch analysis handles individual errors gracefully."""
|
|
trades = [
|
|
create_trade_event(trade_id="trade1", size=Decimal("3000")),
|
|
create_trade_event(trade_id="trade2", size=Decimal("4000")),
|
|
]
|
|
|
|
# First call succeeds, second fails
|
|
profile = create_wallet_profile(nonce=2, age_hours=24.0)
|
|
mock_wallet_analyzer.analyze.side_effect = [profile, Exception("Error")]
|
|
|
|
results = await detector.analyze_batch(trades)
|
|
|
|
assert len(results) == 1
|
|
|
|
async def test_analyze_batch_empty_list(self, detector):
|
|
"""Test batch analysis with empty list."""
|
|
results = await detector.analyze_batch([])
|
|
|
|
assert results == []
|
|
|
|
async def test_analyze_batch_all_filtered(self, detector, mock_wallet_analyzer):
|
|
"""Test batch analysis when all trades are filtered."""
|
|
trades = [
|
|
create_trade_event(trade_id="trade1", size=Decimal("100")),
|
|
create_trade_event(trade_id="trade2", size=Decimal("50")),
|
|
]
|
|
|
|
results = await detector.analyze_batch(trades)
|
|
|
|
assert results == []
|
|
mock_wallet_analyzer.analyze.assert_not_called()
|
|
|
|
|
|
# === Integration-Style Tests ===
|
|
|
|
|
|
class TestDetectorIntegration:
|
|
"""Integration-style tests for complete detector flow."""
|
|
|
|
async def test_full_detection_flow(self, mock_wallet_analyzer):
|
|
"""Test complete detection flow from trade to signal."""
|
|
detector = FreshWalletDetector(mock_wallet_analyzer)
|
|
|
|
# Create a suspicious trade
|
|
trade = create_trade_event(
|
|
wallet_address="0xfresh123",
|
|
market_id="suspicious_market",
|
|
price=Decimal("0.65"),
|
|
size=Decimal("20000"), # $13,000 notional
|
|
)
|
|
|
|
# Create a fresh wallet profile
|
|
profile = create_wallet_profile(
|
|
address="0xfresh123",
|
|
nonce=0, # Brand new
|
|
age_hours=0.5, # Very young
|
|
)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
# Analyze
|
|
signal = await detector.analyze(trade)
|
|
|
|
# Verify signal
|
|
assert signal is not None
|
|
assert signal.wallet_address == "0xfresh123"
|
|
assert signal.market_id == "suspicious_market"
|
|
assert signal.is_high_confidence is True
|
|
assert "brand_new" in signal.factors
|
|
assert "very_young" in signal.factors
|
|
assert "large_trade" in signal.factors
|
|
|
|
async def test_custom_thresholds(self, mock_wallet_analyzer):
|
|
"""Test detection with custom thresholds."""
|
|
detector = FreshWalletDetector(
|
|
mock_wallet_analyzer,
|
|
min_trade_size=Decimal("5000"),
|
|
max_nonce=3,
|
|
max_age_hours=24.0,
|
|
)
|
|
|
|
# Trade that would pass default but fails custom thresholds
|
|
trade = create_trade_event(size=Decimal("8000")) # $4000 notional
|
|
profile = create_wallet_profile(nonce=4, age_hours=30.0)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
# Should fail min_trade_size check
|
|
result = await detector.analyze(trade)
|
|
assert result is None
|
|
|
|
async def test_edge_case_exact_min_trade_size(self, detector, mock_wallet_analyzer):
|
|
"""Test trade exactly at minimum size threshold."""
|
|
trade = create_trade_event(
|
|
price=Decimal("0.5"),
|
|
size=Decimal("2000"), # $1000 notional exactly at default
|
|
)
|
|
profile = create_wallet_profile(nonce=2)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
# At threshold should pass
|
|
assert result is not None
|
|
|
|
async def test_edge_case_below_min_trade_size(self, detector, mock_wallet_analyzer):
|
|
"""Test trade just below minimum size threshold."""
|
|
trade = create_trade_event(
|
|
price=Decimal("0.5"),
|
|
size=Decimal("1999"), # $999.50 - just under $1000
|
|
)
|
|
profile = create_wallet_profile(nonce=2)
|
|
mock_wallet_analyzer.analyze.return_value = profile
|
|
|
|
result = await detector.analyze(trade)
|
|
|
|
# Below threshold should fail
|
|
assert result is None
|