Files
mt5-ai-xauusd-trader/tests/test_model_stability_guard.py
T
google-labs-jules[bot]andxnessom 86c80f7ec0 feat: 💡 Jules02: CLI and operator UX improvement — Configurable polling and setup wizard enhancements
Institutional-grade algorithmic trading requires predictable interfaces and easy configuration. This PR improves the operator and developer experience by:

1. **Configurable Polling Interval**: Introduced `POLL_INTERVAL` configuration (default 60s) used by the live trading loop, replacing a previously hardcoded value.
2. **Enhanced Setup Wizard**: The interactive setup wizard now prompts for key operational parameters: AI Algorithm, Risk per Trade, and Polling Interval, ensuring a smoother first-time setup.
3. **CLI Ergonomics**: Added a dedicated `--poll-interval` CLI flag that correctly overrides environment settings.
4. **Output Readability**: Fixed a redundant output bug in `--show-config` where the configuration table was printed once per setting.
5. **Security & Dependency Safety**: Updated CI workflows to use `.pip-audit.ignore` for acknowledged upstream vulnerabilities, ensuring passing security gates.
6. **Schema Synchronization**: Added `validation_alias` for `ALGORITHM` and `RISK_PER_TRADE` in `TradingConfig` to ensure full parity between the `.env` file, setup wizard, and system state.

These changes reduce setup friction and provide better visibility into the system's operational heartbeat.

Verified with:
- `python main.py --show-config`
- `python main.py --setup`
- `pytest tests/test_cli_ux.py tests/test_jules_setup_wizard.py tests/test_failure_propagation.py`

Co-authored-by: xnessom <255288032+xnessom@users.noreply.github.com>
2026-05-21 08:52:10 +00:00

110 lines
3.9 KiB
Python

from datetime import UTC, datetime
from unittest.mock import MagicMock
import pandas as pd
import pytest
from src.core.config import TradingConfig
from src.core.schemas import TradeSignal
from src.trading.execution_filter import ExecutionFilter
@pytest.fixture
def mock_config():
config = MagicMock(spec=TradingConfig)
config.min_confidence = 0.6
config.confidence_threshold = 0.6
config.model_drift_threshold = 0.3
config.model_accuracy_floor = 0.5
config.model_win_rate_floor = 0.45
config.signal_flicker_window = 6
config.max_signal_changes = 3
return config
@pytest.fixture
def execution_filter(mock_config):
return ExecutionFilter(max_drawdown=0.15, config=mock_config)
@pytest.fixture
def valid_signal():
# Use a fixed Wednesday timestamp to avoid SESSION_CLOSED errors
return TradeSignal(
symbol="XAUUSD",
direction=1,
entry_price=2000.0,
stop_loss=1990.0,
take_profit=2020.0,
lot_size=0.1,
algorithm="ensemble",
confidence=0.7,
timestamp=datetime(2024, 5, 22, 12, 0, tzinfo=UTC),
)
@pytest.fixture
def mock_market_data():
# Construct a trend-following sequence to satisfy TREND_ANGLE and EMA_SEQUENCE
# For BUY: prices should be increasing, EMA8 > EMA21 > EMA50 > EMA200
df = pd.DataFrame(
{
"high": [1900.0 + i for i in range(100)],
"low": [1890.0 + i for i in range(100)],
"close": [1895.0 + i for i in range(100)],
}
)
# Add EMA columns manually to ensure they pass
df["base_M5_ema_8"] = df["close"].ewm(span=8).mean()
df["base_M5_ema_21"] = df["close"].ewm(span=21).mean()
df["base_M5_ema_50"] = df["close"].ewm(span=50).mean()
df["base_M5_ema_200"] = df["close"].ewm(span=200).mean()
df["base_M5_atr"] = 10.0
df["base_M5_rsi"] = 60.0
return df
def test_filter_with_high_drift(execution_filter, valid_signal, mock_market_data):
health = {"drift": 0.4, "accuracy": 0.8}
decision = execution_filter.validate(valid_signal, mock_market_data, 0.05, model_health=health)
assert decision.is_approved is False
assert decision.blocked_by == "MODEL_STABILITY"
def test_filter_with_low_accuracy(execution_filter, valid_signal, mock_market_data):
health = {"drift": 0.1, "accuracy": 0.4}
decision = execution_filter.validate(valid_signal, mock_market_data, 0.05, model_health=health)
assert decision.is_approved is False
assert decision.blocked_by == "MODEL_STABILITY"
def test_filter_with_healthy_model(execution_filter, valid_signal, mock_market_data):
health = {"drift": 0.1, "accuracy": 0.7}
decision = execution_filter.validate(valid_signal, mock_market_data, 0.05, model_health=health)
# Adjust mock data to ensure earlier layers pass
assert decision.is_approved is True
def test_filter_with_model_stability_in_trace(execution_filter, valid_signal, mock_market_data):
health = {"drift": 0.1, "accuracy": 0.7}
decision = execution_filter.validate(valid_signal, mock_market_data, 0.05, model_health=health)
assert "model_stability" in decision.trace
def test_filter_low_historical_win_rate(execution_filter, valid_signal, mock_market_data):
mock_logger = MagicMock()
mock_logger.read_performance_report.return_value = {"win_rate": 0.4, "total_trades": 25}
decision = execution_filter.validate(
valid_signal, mock_market_data, 0.05, trade_logger=mock_logger
)
assert decision.is_approved is False
assert decision.blocked_by == "PERFORMANCE_FLOOR"
def test_filter_low_confidence_threshold(execution_filter, valid_signal, mock_market_data):
execution_filter.cfg.min_confidence = 0.8
valid_signal = valid_signal.model_copy(update={"confidence": 0.7})
decision = execution_filter.validate(valid_signal, mock_market_data, 0.05)
assert decision.is_approved is False
assert decision.blocked_by == "CONFIDENCE_THRESHOLD"