213 lines
7.1 KiB
Python
213 lines
7.1 KiB
Python
|
|
"""Tests for MetricsMonitor."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from datetime import datetime
|
||
|
|
from unittest.mock import Mock
|
||
|
|
from nautilus_dolphin.nautilus.metrics_monitor import (
|
||
|
|
MetricsMonitor,
|
||
|
|
ThresholdConfig,
|
||
|
|
Alert
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestMetricsMonitor:
|
||
|
|
|
||
|
|
def test_record_fill_updates_counters(self):
|
||
|
|
"""Test fill recording updates maker/taker counters."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
monitor.record_fill('maker', 0.5)
|
||
|
|
monitor.record_fill('maker', 0.3)
|
||
|
|
monitor.record_fill('taker', 1.2)
|
||
|
|
|
||
|
|
assert monitor._maker_fills == 2
|
||
|
|
assert monitor._taker_fills == 1
|
||
|
|
|
||
|
|
def test_get_maker_fill_rate(self):
|
||
|
|
"""Test maker fill rate calculation."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
# Default when no fills
|
||
|
|
assert monitor.get_maker_fill_rate() == 100.0
|
||
|
|
|
||
|
|
# After some fills
|
||
|
|
monitor.record_fill('maker', 0.5)
|
||
|
|
monitor.record_fill('maker', 0.3)
|
||
|
|
monitor.record_fill('taker', 1.2)
|
||
|
|
|
||
|
|
assert monitor.get_maker_fill_rate() == (2/3) * 100
|
||
|
|
|
||
|
|
def test_get_average_slippage(self):
|
||
|
|
"""Test average slippage calculation."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
# Default when no data
|
||
|
|
assert monitor.get_average_slippage() == 0.0
|
||
|
|
|
||
|
|
# After fills
|
||
|
|
monitor.record_fill('maker', 2.0)
|
||
|
|
monitor.record_fill('taker', 4.0)
|
||
|
|
monitor.record_fill('maker', 6.0)
|
||
|
|
|
||
|
|
assert monitor.get_average_slippage() == 4.0
|
||
|
|
|
||
|
|
def test_record_trade_result_updates_pnl(self):
|
||
|
|
"""Test trade result recording updates P&L."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
monitor.record_trade_result(100.0)
|
||
|
|
monitor.record_trade_result(-50.0)
|
||
|
|
monitor.record_trade_result(200.0)
|
||
|
|
|
||
|
|
assert monitor._total_pnl == 250.0
|
||
|
|
assert monitor._winning_trades == 2
|
||
|
|
assert monitor._total_trades == 3
|
||
|
|
|
||
|
|
def test_get_win_rate(self):
|
||
|
|
"""Test win rate calculation."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
# Default when no trades
|
||
|
|
assert monitor.get_win_rate() == 0.0
|
||
|
|
|
||
|
|
monitor.record_trade_result(100.0)
|
||
|
|
monitor.record_trade_result(200.0)
|
||
|
|
monitor.record_trade_result(-50.0)
|
||
|
|
|
||
|
|
assert monitor.get_win_rate() == (2/3) * 100
|
||
|
|
|
||
|
|
def test_get_profit_factor(self):
|
||
|
|
"""Test profit factor calculation."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
monitor.record_trade_result(100.0)
|
||
|
|
monitor.record_trade_result(200.0)
|
||
|
|
monitor.record_trade_result(-50.0)
|
||
|
|
|
||
|
|
assert monitor.get_profit_factor() == 300.0 / 50.0
|
||
|
|
|
||
|
|
def test_profit_factor_no_losses(self):
|
||
|
|
"""Test profit factor when no losses."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
monitor.record_trade_result(100.0)
|
||
|
|
|
||
|
|
assert monitor.get_profit_factor() == float('inf')
|
||
|
|
|
||
|
|
def test_alert_on_low_maker_fill_rate(self):
|
||
|
|
"""Test alert raised when maker fill rate drops below threshold."""
|
||
|
|
config = ThresholdConfig(
|
||
|
|
critical_maker_fill_rate=50.0,
|
||
|
|
warning_maker_fill_rate=60.0
|
||
|
|
)
|
||
|
|
monitor = MetricsMonitor(config)
|
||
|
|
|
||
|
|
# Add many taker fills to drop maker rate below threshold
|
||
|
|
for _ in range(10):
|
||
|
|
monitor.record_fill('taker', 1.0)
|
||
|
|
|
||
|
|
# Check alert was raised
|
||
|
|
alerts = [a for a in monitor._alerts if a.metric == 'maker_fill_rate']
|
||
|
|
assert len(alerts) > 0
|
||
|
|
assert alerts[0].level == 'critical'
|
||
|
|
|
||
|
|
def test_alert_on_high_slippage(self):
|
||
|
|
"""Test alert raised when slippage exceeds threshold."""
|
||
|
|
config = ThresholdConfig(
|
||
|
|
warning_slippage_bps=3.0,
|
||
|
|
critical_slippage_bps=5.0
|
||
|
|
)
|
||
|
|
monitor = MetricsMonitor(config)
|
||
|
|
|
||
|
|
monitor.record_fill('taker', 10.0)
|
||
|
|
|
||
|
|
# Check alert was raised
|
||
|
|
alerts = [a for a in monitor._alerts if a.metric == 'slippage']
|
||
|
|
assert len(alerts) > 0
|
||
|
|
assert alerts[0].level == 'critical'
|
||
|
|
|
||
|
|
def test_alert_deduplication(self):
|
||
|
|
"""Test alerts are deduplicated within 5-minute window."""
|
||
|
|
config = ThresholdConfig(critical_maker_fill_rate=50.0)
|
||
|
|
monitor = MetricsMonitor(config)
|
||
|
|
|
||
|
|
# Add many taker fills
|
||
|
|
for _ in range(20):
|
||
|
|
monitor.record_fill('taker', 1.0)
|
||
|
|
|
||
|
|
# Should only have 1 alert (deduplicated)
|
||
|
|
alerts = [a for a in monitor._alerts if a.metric == 'maker_fill_rate']
|
||
|
|
assert len(alerts) == 1
|
||
|
|
|
||
|
|
def test_add_alert_handler(self):
|
||
|
|
"""Test custom alert handler registration."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
handler = Mock()
|
||
|
|
|
||
|
|
monitor.add_alert_handler(handler)
|
||
|
|
|
||
|
|
# Trigger an alert
|
||
|
|
config = ThresholdConfig(critical_maker_fill_rate=50.0)
|
||
|
|
monitor.config = config
|
||
|
|
for _ in range(10):
|
||
|
|
monitor.record_fill('taker', 1.0)
|
||
|
|
|
||
|
|
# Handler should have been called
|
||
|
|
assert handler.called
|
||
|
|
|
||
|
|
def test_get_metrics_summary(self):
|
||
|
|
"""Test metrics summary report."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
monitor.record_fill('maker', 0.5)
|
||
|
|
monitor.record_trade_result(100.0)
|
||
|
|
|
||
|
|
summary = monitor.get_metrics_summary()
|
||
|
|
|
||
|
|
assert 'maker_fill_rate_pct' in summary
|
||
|
|
assert 'average_slippage_bps' in summary
|
||
|
|
assert 'total_trades' in summary
|
||
|
|
assert 'win_rate_pct' in summary
|
||
|
|
assert 'profit_factor' in summary
|
||
|
|
assert 'total_pnl' in summary
|
||
|
|
|
||
|
|
def test_prometheus_export(self):
|
||
|
|
"""Test Prometheus format export."""
|
||
|
|
monitor = MetricsMonitor()
|
||
|
|
|
||
|
|
monitor.record_fill('maker', 0.5)
|
||
|
|
monitor.record_trade_result(100.0)
|
||
|
|
|
||
|
|
prometheus = monitor.get_prometheus_metrics()
|
||
|
|
|
||
|
|
assert 'dolphin_maker_fill_rate_pct' in prometheus
|
||
|
|
assert 'dolphin_slippage_bps' in prometheus
|
||
|
|
assert 'dolphin_total_trades' in prometheus
|
||
|
|
assert 'dolphin_win_rate_pct' in prometheus
|
||
|
|
assert 'dolphin_profit_factor' in prometheus
|
||
|
|
assert 'dolphin_total_pnl' in prometheus
|
||
|
|
|
||
|
|
|
||
|
|
class TestThresholdConfig:
|
||
|
|
|
||
|
|
def test_default_thresholds(self):
|
||
|
|
"""Test default threshold configuration."""
|
||
|
|
config = ThresholdConfig()
|
||
|
|
|
||
|
|
assert config.min_maker_fill_rate == 48.0
|
||
|
|
assert config.max_slippage_bps == 5.0
|
||
|
|
assert config.critical_maker_fill_rate == 48.0
|
||
|
|
assert config.warning_slippage_bps == 3.0
|
||
|
|
|
||
|
|
def test_custom_thresholds(self):
|
||
|
|
"""Test custom threshold configuration."""
|
||
|
|
config = ThresholdConfig(
|
||
|
|
min_maker_fill_rate=60.0,
|
||
|
|
max_slippage_bps=3.0,
|
||
|
|
critical_maker_fill_rate=55.0
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.min_maker_fill_rate == 60.0
|
||
|
|
assert config.max_slippage_bps == 3.0
|
||
|
|
assert config.critical_maker_fill_rate == 55.0
|