fix(detector): suppress niche-only signals under a min trade size (#100)

The size anomaly detector currently emits a base 0.2 confidence signal
whenever a market is "niche" (low volume OR niche-prone category with
unknown volume) — even for $5 trades. In production this floods the
alert pipeline with nothing-burgers, because the niche-prone category
set covers `science / tech / finance / other`, which matches a huge
chunk of Polymarket's long tail.

This adds a `DEFAULT_NICHE_MIN_TRADE_SIZE = $500` floor that applies
ONLY to the niche-only path: if a trade exceeds the volume or book
thresholds, the guard does not block it. So real anomalies still come
through, but tiny niche trades get filtered out before reaching the
risk scorer.

The threshold is configurable via `niche_min_trade_size` in
`SizeAnomalyDetector.__init__`.

Tests added:
- niche-only below floor → suppressed
- niche-only at/above floor → still emits 0.2 base
- small trade with high volume_impact → guard does not block

Co-authored-by: schrodinger01 <schrodinger01@users.noreply.github.com>
This commit is contained in:
schrodinger01
2026-06-17 15:48:01 -04:00
committed by GitHub
co-authored by schrodinger01
parent 5f5a2bffd6
commit 0ceae95e92
2 changed files with 107 additions and 0 deletions
+87
View File
@@ -588,6 +588,93 @@ class TestAnalyzeMethod:
assert signal.is_niche_market is True
assert signal.confidence == 0.2 # niche_base
@pytest.mark.asyncio
async def test_analyze_niche_only_below_min_trade_size_skipped(
self,
mock_metadata_sync: AsyncMock,
sample_metadata: MarketMetadata,
) -> None:
"""Niche-only trades below the min trade size are suppressed."""
mock_metadata_sync.get_market.return_value = sample_metadata
detector = SizeAnomalyDetector(mock_metadata_sync)
tiny_trade = TradeEvent(
market_id="market_abc123",
trade_id="tx_tiny",
wallet_address="0xabc",
side="BUY",
outcome="Yes",
outcome_index=0,
price=Decimal("0.5"),
size=Decimal("100"), # $50 notional, below default $500 floor
timestamp=datetime.now(UTC),
asset_id="token_123",
event_title="Niche tiny trade",
)
signal = await detector.analyze(tiny_trade)
assert signal is None
@pytest.mark.asyncio
async def test_analyze_niche_only_at_min_trade_size_emits(
self,
mock_metadata_sync: AsyncMock,
sample_metadata: MarketMetadata,
) -> None:
"""Niche-only trades at or above the min trade size still emit."""
mock_metadata_sync.get_market.return_value = sample_metadata
detector = SizeAnomalyDetector(mock_metadata_sync)
ok_trade = TradeEvent(
market_id="market_abc123",
trade_id="tx_ok",
wallet_address="0xabc",
side="BUY",
outcome="Yes",
outcome_index=0,
price=Decimal("0.5"),
size=Decimal("2000"), # $1000 notional, above default $500 floor
timestamp=datetime.now(UTC),
asset_id="token_123",
event_title="Niche ok trade",
)
signal = await detector.analyze(ok_trade)
assert signal is not None
assert signal.is_niche_market is True
assert signal.confidence == 0.2
@pytest.mark.asyncio
async def test_niche_min_trade_size_does_not_block_real_anomalies(
self,
mock_metadata_sync: AsyncMock,
sample_metadata: MarketMetadata,
) -> None:
"""A trade that exceeds volume/book thresholds is never blocked by the niche guard."""
mock_metadata_sync.get_market.return_value = sample_metadata
detector = SizeAnomalyDetector(mock_metadata_sync)
small_but_high_impact_trade = TradeEvent(
market_id="market_abc123",
trade_id="tx_small_impact",
wallet_address="0xabc",
side="BUY",
outcome="Yes",
outcome_index=0,
price=Decimal("0.5"),
size=Decimal("200"), # $100 notional, below niche floor
timestamp=datetime.now(UTC),
asset_id="token_123",
event_title="Small but high-impact",
)
# Provide small daily_volume so volume_impact exceeds 2% threshold
signal = await detector.analyze(
small_but_high_impact_trade, daily_volume=Decimal("1000")
)
assert signal is not None
assert signal.volume_impact > 0.02
@pytest.mark.asyncio
async def test_analyze_no_anomaly(
self,