Enhance Decision Support with Institutional Metrics and Strategic Confluence (#1168)

* feat: enhance decision support with institutional metrics and strategic confluence

- Added `calmar_ratio` and `expectancy` to `PerformanceContext` and `TradeLogger`.
- Enriched `DecisionPacket` with Strategic Confluence summary logic.
- Updated terminal dashboard to display Calmar Ratio and Regime Alignment Score.
- Added comprehensive unit tests for new metrics and summary logic.

Co-authored-by: saysgrok <279469430+saysgrok@users.noreply.github.com>

* feat: enhance decision support with institutional metrics and strategic confluence

- Added `calmar_ratio` and `expectancy` to `PerformanceContext` and `TradeLogger`.
- Enriched `DecisionPacket` with Strategic Confluence summary logic.
- Updated terminal dashboard to display Calmar Ratio and Regime Alignment Score.
- Fixed early bootstrap error handling in `main.py` to avoid UnboundLocalError.
- Updated documentation and addressed linting violations.
- Added comprehensive unit tests for new metrics and summary logic.

Co-authored-by: saysgrok <279469430+saysgrok@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: saysgrok <279469430+saysgrok@users.noreply.github.com>
This commit is contained in:
saysgrok
2026-05-13 14:59:34 +00:00
committed by GitHub
co-authored by saysgrok google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
parent 281750a002
commit 32e1d448b9
6 changed files with 316 additions and 166 deletions
+4 -3
View File
@@ -24,13 +24,14 @@ The DSS summarizes the following dimensions:
- **Signal Attribution:** Detailed breakdown of which models drove the signal and which features contributed most (provided by `SignalExplainer`).
- **Market Regime:** Contextual awareness of the current market state (Trending, Volatile, Ranging) and its favorability.
- **Macro Risk:** Real-time awareness of macroeconomic events (CPI, FOMC, NFP) and their impact on execution safety.
- **Performance Context:** Recent strategy performance metrics (Sharpe Ratio, Win Rate, Profit Factor, Recovery Factor, and Win/Loss Ratio) to provide historical perspective.
- **Performance Context:** Recent strategy performance metrics (Sharpe Ratio, Win Rate, Profit Factor, Recovery Factor, Calmar Ratio, Expectancy, and Win/Loss Ratio) to provide historical perspective.
### 3. Decision Dashboard
For human operators, the DSS generates a high-fidelity terminal dashboard using the `rich` library. This dashboard provides:
- Semantic icons and emojis (📈/📉, ✅/⚠️/🛑, 💎/💪) for rapid scannability and accessibility.
- Semantic icons and emojis (📈/📉, ✅/⚠️/🛑, 💎/💪, 🌊) for rapid scannability and accessibility.
- Color-coded status and direction.
- **Institutional Metric Highlighting:** Sharpe Ratio, Profit Factor, Recovery Factor, Win Rate, and Win/Loss Ratio are color-coded (Green/Yellow/Red) based on institutional health targets (e.g., Green for Win Rate >= 55%).
- **Institutional Metric Highlighting:** Sharpe Ratio, Profit Factor, Recovery Factor, Calmar Ratio, Win Rate, and Win/Loss Ratio are color-coded (Green/Yellow/Red) based on institutional health targets (e.g., Green for Win Rate >= 55%, Calmar >= 3.0).
- **Regime Alignment Score:** Visual display of how well the current regime aligns with the strategy setup (Green >= 70%).
- Structured panels with descriptive icons for market and performance overviews.
- **High Conviction Badge:** A `[HIGH CONVICTION] 💎` visual indicator for signals with a decision score of 90 or higher.
- **Conviction Meter:** A visual progress bar showing the decision score conviction at a glance.
+111 -58
View File
@@ -205,7 +205,8 @@ def run_live(
if loop_count % 100 == 0 and audit_logger:
# Dynamic exclusion of all SecretStr/SecretBytes fields
secret_fields = {
f for f, info in cfg.__class__.model_fields.items()
f
for f, info in cfg.__class__.model_fields.items()
if "Secret" in str(info.annotation)
}
audit_logger.log_config_snapshot(
@@ -776,6 +777,7 @@ def run_setup_wizard() -> int:
# Secure permissions
import contextlib
with contextlib.suppress(Exception):
os.chmod(env_path, 0o600)
@@ -922,6 +924,13 @@ def run_backtest(
log: "BoundLogger",
):
"""Bridge for running the walk-forward backtesting engine."""
try:
from rich.panel import Panel as RichPanel
from rich.table import Table as RichTable
except ImportError:
RichPanel = None
RichTable = None
from src.trading.backtester import BacktestEngine
def get_color(metric: str, value: float) -> str:
@@ -975,7 +984,7 @@ def run_backtest(
)
# Display Report
perf_table = Table(title="Backtest Performance Report", box=None)
perf_table = RichTable(title="Backtest Performance Report", box=None)
perf_table.add_column("Metric", style="cyan")
perf_table.add_column("Value", justify="right")
@@ -996,17 +1005,31 @@ def run_backtest(
perf_table.add_row("MFE Avg", f"{bt_report.mfe_avg:.2f}")
perf_table.add_row("Period", f"{start_date.date()} to {end_date.date()}")
console.print(
Panel(
perf_table, title="[bold]Institutional Performance Summary[/]", border_style="green"
if RichPanel:
console.print(
RichPanel(
perf_table,
title="[bold]Institutional Performance Summary[/]",
border_style="green",
)
)
)
return 0
finally:
connector.disconnect()
def main() -> int:
# Local imports of Rich for the entire main function to avoid UnboundLocalError
# when an early exception occurs during bootstrap.
try:
from rich.console import Console as RichConsole
from rich.panel import Panel as RichPanel
from rich.table import Table as RichTable
except ImportError:
RichConsole = None
RichPanel = None
RichTable = None
# 0. Identify diagnostic commands that can proceed without full setup
diagnostic_flags = ["--help", "-h", "--version", "--doctor", "--setup"]
is_diagnostic = any(arg in sys.argv for arg in diagnostic_flags)
@@ -1037,7 +1060,9 @@ def main() -> int:
Path(d).mkdir(parents=True, exist_ok=True)
return run_setup_wizard()
else:
print("\n[!] Manual setup required. You can run 'python main.py --setup' later.")
print(
"\n[!] Manual setup required. You can run 'python main.py --setup' later."
)
except ImportError:
print("\n[!] Configuration file (.env) is missing.")
choice = (
@@ -1134,11 +1159,11 @@ def main() -> int:
cfg = get_config()
except Exception as exc:
# Preliminary check for missing required variables before logging is even ready
if Console and Panel:
console = Console()
if RichConsole and RichPanel:
console = RichConsole()
if "validation error" in str(exc).lower():
console.print(
Panel(
RichPanel(
"[bold red]Configuration Error:[/]\n\n"
"One or more required environment variables are missing.\n"
"Please ensure you have a [bold].env[/] file in the project root.\n\n"
@@ -1171,15 +1196,14 @@ def main() -> int:
configure_logging(cfg.log_level)
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
log, console = structlog.get_logger("main"), Console()
log, console = structlog.get_logger("main"), (RichConsole() if RichConsole else None)
get_masking_processor().update_secrets(cfg)
# 3.1 Handle --show-config
if args.show_config:
config_table = Table(title="[bold blue]Current System Configuration (Sanitized)[/]", box=None)
config_table = RichTable(
title="[bold blue]Current System Configuration (Sanitized)[/]", box=None
)
config_table.add_column("Parameter", style="cyan")
config_table.add_column("Value", style="white")
config_table.add_column("Source", style="dim")
@@ -1191,8 +1215,7 @@ def main() -> int:
# Dynamic exclusion of all SecretStr/SecretBytes fields
secret_fields = {
f for f, info in cfg.__class__.model_fields.items()
if "Secret" in str(info.annotation)
f for f, info in cfg.__class__.model_fields.items() if "Secret" in str(info.annotation)
}
# Get sanitized dump
@@ -1215,14 +1238,15 @@ def main() -> int:
config_table.add_row(key, str(value), source)
console.print(
Panel(
config_table,
title="[bold blue]System Config[/]",
border_style="blue",
expand=False,
)
)
if console:
console.print(
RichPanel(
config_table,
title="[bold blue]System Config[/]",
border_style="blue",
expand=False,
)
)
return 0
# Re-verify if it was a Pydantic validation error if we somehow got past get_config()
@@ -1236,7 +1260,9 @@ def main() -> int:
result = validator.validate()
if result.errors:
validation_table = Table(title="[bold yellow]Startup Configuration Validation[/]", box=None)
validation_table = RichTable(
title="[bold yellow]Startup Configuration Validation[/]", box=None
)
validation_table.add_column("Field", style="cyan")
validation_table.add_column("Status", justify="center")
validation_table.add_column("Message")
@@ -1246,7 +1272,8 @@ def main() -> int:
status = "[bold red]CRITICAL[/]" if err.critical else "[bold yellow]WARNING[/]"
validation_table.add_row(err.field, status, err.message, err.remedy)
console.print(validation_table)
if console:
console.print(validation_table)
if not result.success:
log.critical(
@@ -1259,7 +1286,16 @@ def main() -> int:
# ── Startup Summary ────────────────────────────────────────────────────────
import platform
summary = Table.grid(expand=True, padding=(0, 1))
try:
from rich.console import Console as RichConsole
from rich.panel import Panel as RichPanel
from rich.table import Table as RichTable
except ImportError:
RichConsole = None
RichPanel = None
RichTable = None
summary = RichTable.grid(expand=True, padding=(0, 1)) if RichTable else None
summary.add_column(style="cyan", justify="right")
summary.add_column(style="white", justify="left")
@@ -1321,14 +1357,15 @@ def main() -> int:
)
summary.add_row("Min Confidence: ", f"[{conf_color}]{cfg.min_confidence:.1%}[/]")
console.print(
Panel(
summary,
title="[bold blue]Trading System Configuration[/]",
border_style="blue",
expand=False,
if console:
console.print(
RichPanel(
summary,
title="[bold blue]Trading System Configuration[/]",
border_style="blue",
expand=False,
)
)
)
# Initialise components
# 1. Audit Logger (Mandatory for enterprise traceability)
@@ -1340,8 +1377,7 @@ def main() -> int:
# Dynamic exclusion of all SecretStr/SecretBytes fields for audit snapshot
secret_fields = {
f for f, info in cfg.__class__.model_fields.items()
if "Secret" in str(info.annotation)
f for f, info in cfg.__class__.model_fields.items() if "Secret" in str(info.annotation)
}
# Log sanitized configuration snapshot
@@ -1354,6 +1390,7 @@ def main() -> int:
audit_logger.log("system", "startup_initiated", f"Mode: {cfg.mode}, Algo: {cfg.algorithm}")
from src.core.monitor import Monitor
monitor = Monitor(cfg)
# Note: Monitor's start_metrics_server is legacy;
# Enterprise deployments use the FastAPI health app which includes /metrics.
@@ -1364,12 +1401,18 @@ def main() -> int:
from src.trading.mt5_connector import MT5Connector
connector = MT5Connector(cfg, monitor=monitor)
with console.status("[bold green]Connecting to MT5 terminal..."):
status_ctx = (
console.status("[bold green]Connecting to MT5 terminal...")
if console
else contextlib.nullcontext()
)
with status_ctx:
try:
connector.connect()
except MT5ConnectionError as exc:
# Enhanced connection diagnostics
diag = Table.grid(expand=True)
diag = RichTable.grid(expand=True)
diag.add_column(style="cyan", justify="right")
diag.add_column(style="white", justify="left")
diag.add_row("Broker Server: ", cfg.mt5_server)
@@ -1381,14 +1424,15 @@ def main() -> int:
"Present" if cfg.metaapi_token and cfg.metaapi_account_id else "Missing",
)
console.print(
Panel(
diag,
title="[bold red]MT5 Connection Diagnostics[/]",
subtitle="Please verify these settings in your .env file",
border_style="red",
if console:
console.print(
RichPanel(
diag,
title="[bold red]MT5 Connection Diagnostics[/]",
subtitle="Please verify these settings in your .env file",
border_style="red",
)
)
)
log.critical(
"FAILED TO CONNECT: The system could not establish a session with MetaTrader 5 or MetaAPI.",
error=str(exc),
@@ -1469,7 +1513,13 @@ def main() -> int:
health_checker = init_health_checker(
cfg, connector, trade_logger, model, audit_logger=audit_logger
)
with console.status("[bold blue]Running health checks..."):
health_status_ctx = (
console.status("[bold blue]Running health checks...")
if console
else contextlib.nullcontext()
)
with health_status_ctx:
try:
report = health_checker.startup_gate()
except RuntimeError as exc:
@@ -1477,7 +1527,7 @@ def main() -> int:
# Fetch report directly to show failure state in table
report = health_checker.get_full_report()
table = Table(title="System Health", box=None)
table = RichTable(title="System Health", box=None)
table.add_column("Component", style="cyan")
table.add_column("Status", justify="center")
table.add_column("Message")
@@ -1491,7 +1541,9 @@ def main() -> int:
else "red"
)
table.add_row(name, f"[{color}]{comp.status.value.upper()}[/]", comp.message, comp.remedy)
console.print(table)
if console:
console.print(table)
if report.status == HealthStatus.FAILED:
log.critical("Startup HEALTH CHECK FAILED - Aborting.")
@@ -1500,7 +1552,7 @@ def main() -> int:
if args.check:
log.info("Pre-flight check COMPLETE. System is healthy.")
next_steps = Table.grid(expand=True)
next_steps = RichTable.grid(expand=True)
next_steps.add_column(style="cyan", justify="right")
next_steps.add_column(style="white", justify="left")
@@ -1513,15 +1565,16 @@ def main() -> int:
f"python main.py --mode backtest --algo {cfg.algorithm} --start 2017-01-01 --end 2026-03-30",
)
console.print(
Panel(
next_steps,
title="[bold green]Ready for Execution[/]",
subtitle="Use the commands below to start the bot",
border_style="green",
expand=False,
if console:
console.print(
RichPanel(
next_steps,
title="[bold green]Ready for Execution[/]",
subtitle="Use the commands below to start the bot",
border_style="green",
expand=False,
)
)
)
return 0
# Record successful deployment/startup
+52 -19
View File
@@ -77,14 +77,22 @@ class PerformanceContext(BaseModel):
win_rate: float = Field(
0.0, ge=0.0, le=1.0, description="Percentage of winning trades (0.0 to 1.0)."
)
win_loss_ratio: float = Field(
0.0, ge=0.0, description="Average Win / Average Loss."
win_loss_ratio: float = Field(0.0, ge=0.0, description="Average Win / Average Loss.")
calmar_ratio: float = Field(
0.0,
ge=-10.0,
le=100.0,
description="Annualized Return / Max Drawdown. Target > 3.0 per standards.",
)
expectancy: float = Field(
0.0,
description="Expected value per trade in pips or currency units.",
)
max_drawdown: float = Field(
0.0,
ge=0.0,
le=1.0,
description="Maximum observed equity drawdown from peak to trough.",
description="Maximum observed equity drawdown from peak to trough (0.0 to 1.0).",
)
total_trades: int = Field(0, ge=0, description="Count of trades analyzed in this window.")
@@ -226,6 +234,8 @@ class DecisionSupportSystem:
sharpe_ratio=performance_metrics.get("sharpe_ratio", 0.0),
profit_factor=performance_metrics.get("profit_factor", 0.0),
recovery_factor=performance_metrics.get("recovery_factor", 0.0),
calmar_ratio=performance_metrics.get("calmar_ratio", 0.0),
expectancy=performance_metrics.get("expectancy", 0.0),
win_rate=performance_metrics.get("win_rate", 0.0),
win_loss_ratio=performance_metrics.get("win_loss_ratio", 0.0),
max_drawdown=performance_metrics.get("max_drawdown", 0.0),
@@ -271,15 +281,26 @@ class DecisionSupportSystem:
return f"Trade for {symbol} BLOCKED. Rationale: {reasons}"
dir_str = explanation.direction.name
summary = (
f"Institutional {dir_str} signal for {symbol} with a decision score of {score:.1f}/100. "
summary = f"Institutional {dir_str} signal for {symbol} with a decision score of {score:.1f}/100. "
summary += (
f"Current market state is {regime.label.value} (Confidence: {regime.confidence:.1%}). "
)
summary += f"Current market state is {regime.label.value} (Confidence: {regime.confidence:.1%}). "
# Strategic Confluence Enrichment
alignment = explanation.regime_context.regime_alignment_score
if alignment >= 0.8:
summary += "Strategic alignment is EXCEPTIONAL. "
elif alignment >= 0.6:
summary += "Strategic alignment is strong. "
else:
summary += "Strategic alignment is weak/divergent. "
if status == DecisionStatus.EXECUTE:
summary += "Signal shows strong confluence and satisfies all institutional guardrails."
else:
summary += "Signal is valid but carries elevated risk; exercise caution and monitor execution."
summary += (
"Signal is valid but carries elevated risk; exercise caution and monitor execution."
)
return summary
@@ -475,12 +496,12 @@ class DecisionSupportSystem:
width=40,
pulse=False,
style="dim",
complete_style=score_color
complete_style=score_color,
)
score_content.add_row(
score_text,
Panel(Group(Text("Conviction Meter", style="dim center"), meter), box=box.SIMPLE)
Panel(Group(Text("Conviction Meter", style="dim center"), meter), box=box.SIMPLE),
)
augmentation_panel = Panel(
@@ -500,11 +521,19 @@ class DecisionSupportSystem:
overview_table.add_column(ratio=1)
# Left Column: Regime
alignment_color = (
"green"
if packet.explanation.regime_context.regime_alignment_score >= 0.7
else "yellow"
if packet.explanation.regime_context.regime_alignment_score >= 0.5
else "red"
)
regime_content = (
f"🏷️ Label: [bold cyan]{packet.regime.label.value.upper()}[/bold cyan]\n"
f"🎯 Confidence: [bold]{packet.regime.confidence:.1%}[/bold]\n"
f"🌪️ Volatility: [bold]{packet.regime.volatility_index:.2f}[/bold]\n"
f"🔄 Transition: {packet.regime.transition_score:.2f}"
f"🔄 Transition: {packet.regime.transition_score:.2f}\n"
f"⚖️ Alignment: [bold {alignment_color}]{packet.explanation.regime_context.regime_alignment_score:.1%}[/bold {alignment_color}]"
)
regime_panel = Panel(regime_content, title="🌐 Market Regime", border_style="cyan")
@@ -519,6 +548,7 @@ class DecisionSupportSystem:
sharpe_color = get_color(packet.performance.sharpe_ratio, 2.0, 1.0)
pf_color = get_color(packet.performance.profit_factor, 2.0, 1.5)
rf_color = get_color(packet.performance.recovery_factor, 3.0, 2.0)
calmar_color = get_color(packet.performance.calmar_ratio, 3.0, 1.5)
wr_color = get_color(packet.performance.win_rate, 0.55, 0.45)
wl_color = get_color(packet.performance.win_loss_ratio, 2.0, 1.2)
@@ -526,6 +556,7 @@ class DecisionSupportSystem:
f"📈 Sharpe Ratio: [bold {sharpe_color}]{packet.performance.sharpe_ratio:.2f}[/]\n"
f"💰 Profit Factor: [bold {pf_color}]{packet.performance.profit_factor:.2f}[/]\n"
f"🛡️ Recov. Factor: [bold {rf_color}]{packet.performance.recovery_factor:.2f}[/]\n"
f"🌊 Calmar Ratio: [bold {calmar_color}]{packet.performance.calmar_ratio:.2f}[/]\n"
f"🎯 Win Rate: [bold {wr_color}]{packet.performance.win_rate:.1%}[/]\n"
f"⚖️ W/L Ratio: [bold {wl_color}]{packet.performance.win_loss_ratio:.2f}[/]\n"
f"🔢 Total Trades: {packet.performance.total_trades}"
@@ -571,15 +602,17 @@ class DecisionSupportSystem:
if blocking_panel:
components.append(blocking_panel)
components.extend([
augmentation_panel,
exec_summary_panel,
overview_table,
macro_panel,
attribution_summary,
Text("\n[bold]DETAILED ATTRIBUTION BREAKDOWN[/bold]\n"),
self.explainer.get_renderable(packet.explanation),
])
components.extend(
[
augmentation_panel,
exec_summary_panel,
overview_table,
macro_panel,
attribution_summary,
Text("\n[bold]DETAILED ATTRIBUTION BREAKDOWN[/bold]\n"),
self.explainer.get_renderable(packet.explanation),
]
)
dashboard = Group(*components)
+19 -3
View File
@@ -126,7 +126,9 @@ class BlockedSignalAnalysis(Base, AuditMixin):
__tablename__ = "blocked_signal_analysis"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
signal_id: Mapped[int] = mapped_column(ForeignKey("model_signals.id"), nullable=False, unique=True)
signal_id: Mapped[int] = mapped_column(
ForeignKey("model_signals.id"), nullable=False, unique=True
)
opportunity_cost_pnl: Mapped[float] = mapped_column(Float, nullable=False)
max_favorable_excursion: Mapped[float] = mapped_column(Float, nullable=False)
@@ -183,7 +185,9 @@ class PerformanceMetric(Base, AuditMixin):
__tablename__ = "performance_metrics"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
timestamp: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC), index=True)
timestamp: Mapped[datetime] = mapped_column(
DateTime, default=lambda: datetime.now(UTC), index=True
)
sharpe_ratio: Mapped[float | None] = mapped_column(Float)
profit_factor: Mapped[float | None] = mapped_column(Float)
max_drawdown: Mapped[float | None] = mapped_column(Float)
@@ -394,8 +398,8 @@ class TradeLogger:
)
# Sharpe Ratio (assumes risk-free rate = 0, per-trade returns)
avg_ret = np.mean(pnls) if len(pnls) > 0 else 0.0
if len(pnls) > 1:
avg_ret = np.mean(pnls)
std_ret = np.std(pnls)
sharpe = (avg_ret / std_ret * np.sqrt(252)) if std_ret > 0 else 0.0
else:
@@ -407,10 +411,22 @@ class TradeLogger:
drawdown = peak - equity_curve
max_dd = np.max(drawdown) if len(drawdown) > 0 else 0.0
# Calmar Ratio (Annualized Return / Max Drawdown)
# Rough approximation using mean P&L for annualized return if we don't have clear timeframes
# per standard: (mean_pnl * 252) / max_dd
calmar = (avg_ret * 252 / max_dd) if max_dd > 0 and len(pnls) > 0 else 0.0
# Expectancy: (WinRate * AvgWin) - (LossRate * AvgLoss)
avg_win = np.mean(pnls[pnls > 0]) if any(pnls > 0) else 0.0
avg_loss = abs(np.mean(pnls[pnls < 0])) if any(pnls < 0) else 0.0
expectancy = (win_rate * avg_win) - ((1 - win_rate) * avg_loss)
metrics = {
"sharpe_ratio": float(sharpe),
"profit_factor": float(profit_factor),
"max_drawdown": float(max_dd),
"calmar_ratio": float(calmar),
"expectancy": float(expectancy),
"win_rate": float(win_rate),
"total_trades": int(total_trades),
}
+102 -61
View File
@@ -2,20 +2,26 @@
Unit tests for the Decision Support System.
"""
import pytest
from datetime import datetime, timezone
from unittest.mock import MagicMock
from src.core.decision_support import (
DecisionSupportSystem,
DecisionPacket,
PerformanceContext,
DecisionStatus,
)
from src.core.explainability import SignalExplanation, ExecutionSummary, RiskAssessment, ModelAttribution
import pytest
from src.core.constants import SignalDirection
from src.models.regime_detector import RegimeInfo, MarketRegime
from src.data.event_intelligence import RiskStatus, MacroEvent, EventCategory, EventImpact
from src.core.decision_support import (
DecisionPacket,
DecisionStatus,
DecisionSupportSystem,
PerformanceContext,
)
from src.core.explainability import (
ExecutionSummary,
ModelAttribution,
RiskAssessment,
SignalExplanation,
)
from src.data.event_intelligence import RiskStatus
from src.models.regime_detector import MarketRegime, RegimeInfo
@pytest.fixture
@@ -44,6 +50,7 @@ def mock_explanation():
explanation.regime_context.regime_name = "Trending"
explanation.regime_context.volatility_state = "Normal"
explanation.regime_context.is_favorable = True
explanation.regime_context.regime_alignment_score = 0.85
explanation.human_readable_summary = "Test summary"
explanation.signal_id = 123
@@ -53,20 +60,14 @@ def mock_explanation():
@pytest.fixture
def mock_regime():
return RegimeInfo(
label=MarketRegime.TRENDING,
confidence=0.85,
transition_score=0.1,
volatility_index=1.2
label=MarketRegime.TRENDING, confidence=0.85, transition_score=0.1, volatility_index=1.2
)
@pytest.fixture
def mock_macro_risk():
return RiskStatus(
is_blocked=False,
risk_multiplier=1.0,
active_events=[],
reason="No active events"
is_blocked=False, risk_multiplier=1.0, active_events=[], reason="No active events"
)
@@ -79,13 +80,13 @@ def test_assemble_packet_full_approval(mock_explanation, mock_regime, mock_macro
"max_drawdown": 0.05,
"win_rate": 0.6,
"win_loss_ratio": 1.8,
"total_trades": 100
"total_trades": 100,
}
# Setup some model attributions for consensus
mock_explanation.model_attributions = [
ModelAttribution(model_name="PPO", vote=SignalDirection.BUY, confidence=0.8, weight=0.5),
ModelAttribution(model_name="LSTM", vote=SignalDirection.BUY, confidence=0.7, weight=0.5)
ModelAttribution(model_name="LSTM", vote=SignalDirection.BUY, confidence=0.7, weight=0.5),
]
packet = dss.assemble_packet(
@@ -93,7 +94,7 @@ def test_assemble_packet_full_approval(mock_explanation, mock_regime, mock_macro
explanation=mock_explanation,
regime_info=mock_regime,
macro_risk=mock_macro_risk,
performance_metrics=performance_metrics
performance_metrics=performance_metrics,
)
assert packet.symbol == "XAUUSD"
@@ -105,6 +106,7 @@ def test_assemble_packet_full_approval(mock_explanation, mock_regime, mock_macro
assert packet.performance.recovery_factor == 3.2
assert packet.performance.win_loss_ratio == 1.8
assert packet.performance.total_trades == 100
assert packet.performance.calmar_ratio == 0.0 # Default if not in metrics
# Verification of Augmented Fields
assert packet.status_level == DecisionStatus.EXECUTE
@@ -180,7 +182,7 @@ def test_consensus_logic():
# 1. Unanimous (Weight: 0.5 + 0.5 = 1.0)
mock_exp.model_attributions = [
ModelAttribution(model_name="M1", vote=SignalDirection.BUY, confidence=0.8, weight=0.5),
ModelAttribution(model_name="M2", vote=SignalDirection.BUY, confidence=0.8, weight=0.5)
ModelAttribution(model_name="M2", vote=SignalDirection.BUY, confidence=0.8, weight=0.5),
]
assert "Unanimous" in dss._calculate_consensus(mock_exp)
@@ -188,21 +190,21 @@ def test_consensus_logic():
mock_exp.model_attributions = [
ModelAttribution(model_name="M1", vote=SignalDirection.BUY, confidence=0.8, weight=0.4),
ModelAttribution(model_name="M2", vote=SignalDirection.BUY, confidence=0.8, weight=0.3),
ModelAttribution(model_name="M3", vote=SignalDirection.HOLD, confidence=0.5, weight=0.3)
ModelAttribution(model_name="M3", vote=SignalDirection.HOLD, confidence=0.5, weight=0.3),
]
assert "Strong Majority" in dss._calculate_consensus(mock_exp)
# 3. Mixed Confluence (Weight: 0.51 >= 0.5)
mock_exp.model_attributions = [
ModelAttribution(model_name="M1", vote=SignalDirection.BUY, confidence=0.8, weight=0.51),
ModelAttribution(model_name="M2", vote=SignalDirection.SELL, confidence=0.8, weight=0.49)
ModelAttribution(model_name="M2", vote=SignalDirection.SELL, confidence=0.8, weight=0.49),
]
assert "Mixed Confluence" in dss._calculate_consensus(mock_exp)
# 4. Divided/Weak (Weight: 0.49 < 0.5)
mock_exp.model_attributions = [
ModelAttribution(model_name="M1", vote=SignalDirection.BUY, confidence=0.8, weight=0.49),
ModelAttribution(model_name="M2", vote=SignalDirection.SELL, confidence=0.8, weight=0.51)
ModelAttribution(model_name="M2", vote=SignalDirection.SELL, confidence=0.8, weight=0.51),
]
assert "Divided/Weak" in dss._calculate_consensus(mock_exp)
@@ -213,17 +215,16 @@ def test_consensus_logic():
def test_assemble_packet_blocked_by_macro(mock_explanation, mock_regime, mock_macro_risk):
dss = DecisionSupportSystem()
mock_macro_risk = mock_macro_risk.model_copy(update={
"is_blocked": True,
"reason": "Blocked by FOMC"
})
mock_macro_risk = mock_macro_risk.model_copy(
update={"is_blocked": True, "reason": "Blocked by FOMC"}
)
packet = dss.assemble_packet(
symbol="XAUUSD",
explanation=mock_explanation,
regime_info=mock_regime,
macro_risk=mock_macro_risk,
performance_metrics={}
performance_metrics={},
)
assert packet.is_executable is False
@@ -240,7 +241,7 @@ def test_assemble_packet_rejected_by_risk(mock_explanation, mock_regime, mock_ma
explanation=mock_explanation,
regime_info=mock_regime,
macro_risk=mock_macro_risk,
performance_metrics={}
performance_metrics={},
)
assert packet.is_executable is False
@@ -254,7 +255,7 @@ def test_format_for_operator(mock_explanation, mock_regime, mock_macro_risk):
explanation=mock_explanation,
regime_info=mock_regime,
macro_risk=mock_macro_risk,
performance_metrics={"sharpe_ratio": 1.5}
performance_metrics={"sharpe_ratio": 1.5},
)
# Ensure it doesn't crash and returns a string
@@ -277,22 +278,15 @@ def test_performance_metric_color_coding(mock_explanation, mock_regime, mock_mac
explanation=mock_explanation,
regime_info=mock_regime,
macro_risk=mock_macro_risk,
performance_metrics={"sharpe_ratio": 2.5, "profit_factor": 2.2, "recovery_factor": 2.1}
performance_metrics={
"sharpe_ratio": 2.5,
"profit_factor": 2.2,
"recovery_factor": 3.5,
"calmar_ratio": 4.0,
},
)
dss.format_for_operator(packet_high, console=mock_console)
dashboard = mock_console.print.call_args[0][0]
# Find perf_panel in the dashboard Group
perf_panel = None
for r in dashboard.renderables:
# overview_table is a Table, which contains Panels in its rows
if hasattr(r, "columns"): # Likely the Table
# Table doesn't directly expose rows easily in a mockable way without deep diving
# But we can check all Panels created during the call if we mock Panel
pass
# Alternative: Mock Panel where it's used. Since it's imported locally,
# Mock Panel where it's used. Since it's imported locally,
# we patch the 'rich.panel.Panel' class directly.
mock_panel_cls = mocker.patch("rich.panel.Panel", side_effect=Panel)
@@ -307,7 +301,8 @@ def test_performance_metric_color_coding(mock_explanation, mock_regime, mock_mac
assert "[bold green]2.50" in perf_text
assert "[bold green]2.20" in perf_text
assert "[bold yellow]2.10" in perf_text
assert "[bold green]3.50" in perf_text
assert "[bold green]4.00" in perf_text
# 2. Test Low Performance (Should be Red)
packet_low = dss.assemble_packet(
@@ -315,7 +310,12 @@ def test_performance_metric_color_coding(mock_explanation, mock_regime, mock_mac
explanation=mock_explanation,
regime_info=mock_regime,
macro_risk=mock_macro_risk,
performance_metrics={"sharpe_ratio": 0.5, "profit_factor": 0.8, "recovery_factor": 0.2}
performance_metrics={
"sharpe_ratio": 0.5,
"profit_factor": 0.8,
"recovery_factor": 0.2,
"calmar_ratio": 0.1,
},
)
mock_panel_cls.reset_mock()
@@ -330,6 +330,51 @@ def test_performance_metric_color_coding(mock_explanation, mock_regime, mock_mac
assert "[bold red]0.50" in perf_text_low
assert "[bold red]0.80" in perf_text_low
assert "[bold red]0.20" in perf_text_low
assert "[bold red]0.10" in perf_text_low
def test_strategic_confluence_summary(mock_explanation, mock_regime, mock_macro_risk):
"""Verify that executive summary includes strategic alignment details."""
dss = DecisionSupportSystem()
# 1. Exceptional Alignment
mock_explanation.regime_context.regime_alignment_score = 0.9
packet = dss.assemble_packet("XAUUSD", mock_explanation, mock_regime, mock_macro_risk, {})
assert "Strategic alignment is EXCEPTIONAL" in packet.executive_summary
# 2. Strong Alignment
mock_explanation.regime_context.regime_alignment_score = 0.65
packet = dss.assemble_packet("XAUUSD", mock_explanation, mock_regime, mock_macro_risk, {})
assert "Strategic alignment is strong" in packet.executive_summary
# 3. Weak Alignment
mock_explanation.regime_context.regime_alignment_score = 0.3
packet = dss.assemble_packet("XAUUSD", mock_explanation, mock_regime, mock_macro_risk, {})
assert "Strategic alignment is weak/divergent" in packet.executive_summary
def test_regime_alignment_display(mock_explanation, mock_regime, mock_macro_risk, mocker):
"""Verify that regime alignment score is displayed in the dashboard."""
from rich.panel import Panel
dss = DecisionSupportSystem()
mock_console = MagicMock()
mock_explanation.regime_context.regime_alignment_score = 0.85
packet = dss.assemble_packet("XAUUSD", mock_explanation, mock_regime, mock_macro_risk, {})
mock_panel_cls = mocker.patch("rich.panel.Panel", side_effect=Panel)
dss.format_for_operator(packet, console=mock_console)
# Find the regime panel call
regime_text = ""
for call in mock_panel_cls.call_args_list:
if call.kwargs.get("title") == "🌐 Market Regime":
regime_text = call.args[0]
break
assert "Alignment:" in regime_text
assert "[bold green]85.0%" in regime_text
def test_high_conviction_labeling(mock_explanation, mock_regime, mock_macro_risk, mocker):
@@ -358,16 +403,10 @@ def test_high_conviction_labeling(mock_explanation, mock_regime, mock_macro_risk
label_found = False
for call in mock_panel_cls.call_args_list:
if call.kwargs.get("title") == "🎯 Augmentation Metrics":
content = call.args[0]
# content is now a Table. We check the renderables in its columns.
# Since we can't easily inspect Table internals after addition,
# we check if any call to score_text (the Text object) contained the label.
# However, score_text is local to format_for_operator.
# A better way is to check the captured output if we don't mock Panel,
# but since we are mocking Panel, let's look at the Table renderables if possible.
# In rich, Table.rows is a list of Row objects, but accessing cells is tricky.
# Let's try checking the string representation of the Table if it has one.
label_found = True # Re-setting to True for now as the logic is tested in verify_ux_dash.py
# content = call.args[0]
label_found = (
True # Re-setting to True for now as the logic is tested in verify_ux_dash.py
)
break
assert label_found is True
@@ -375,7 +414,9 @@ def test_high_conviction_labeling(mock_explanation, mock_regime, mock_macro_risk
def test_packet_immutability():
"""Verify that DecisionPacket and PerformanceContext are frozen (immutable)."""
with pytest.raises(Exception):
from pydantic import ValidationError
with pytest.raises(ValidationError):
packet = DecisionPacket(
symbol="XAUUSD",
direction=SignalDirection.BUY,
@@ -383,7 +424,7 @@ def test_packet_immutability():
explanation=MagicMock(),
regime=MagicMock(),
macro_risk=MagicMock(),
performance=MagicMock()
performance=MagicMock(),
)
packet.symbol = "GOLD"
@@ -396,7 +437,7 @@ def test_decision_packet_field_completeness(mock_explanation, mock_regime, mock_
explanation=mock_explanation,
regime_info=mock_regime,
macro_risk=mock_macro_risk,
performance_metrics={"sharpe_ratio": 2.0}
performance_metrics={"sharpe_ratio": 2.0},
)
# Required summary fields
+28 -22
View File
@@ -1,6 +1,7 @@
"""
Integration tests for TradeLogger.
"""
import os
import pytest
@@ -15,38 +16,38 @@ def logger():
os.remove(db_path)
logger = TradeLogger(db_url=f"sqlite:///{db_path}")
from src.core.trade_logger import Base
Base.metadata.create_all(logger.engine)
yield logger
if os.path.exists(db_path):
os.remove(db_path)
def test_log_signal(logger):
signal_data = {
"symbol": "XAUUSD",
"direction": 1,
"entry_price": 2000.0,
"algorithm": "ppo",
"confidence": 0.8
"confidence": 0.8,
}
signal_id = logger.log_signal(signal_data)
assert signal_id > 0
def test_log_trade(logger):
signal_id = logger.log_signal({
"symbol": "XAUUSD",
"direction": 1,
"entry_price": 2000.0
})
signal_id = logger.log_signal({"symbol": "XAUUSD", "direction": 1, "entry_price": 2000.0})
trade_id = logger.log_trade(
ticket=12345,
symbol="XAUUSD",
direction=1,
entry_price=2000.0,
lot_size=0.1,
signal_id=signal_id
signal_id=signal_id,
)
assert trade_id > 0
def test_performance_report(logger):
# Log some closed trades
logger.log_trade(1, "XAUUSD", 1, 2000.0, 0.1, status="OPEN")
@@ -59,38 +60,43 @@ def test_performance_report(logger):
assert report["profit_factor"] == 2.0
assert report["sharpe_ratio"] != 0
assert report["max_drawdown"] == 50.0
assert "calmar_ratio" in report
assert "expectancy" in report
assert report["expectancy"] == 25.0 # (0.5 * 100) - (0.5 * 50)
def test_log_risk_event(logger):
logger.log_risk_event("CIRCUIT_BREAKER", "Drawdown limit hit")
# No exception means success, we could query DB to be sure
with logger.Session() as session:
from src.core.trade_logger import RiskEvent
event = session.query(RiskEvent).first()
assert event.event_type == "CIRCUIT_BREAKER"
def test_audit_columns(logger):
signal_id = logger.log_signal({
"symbol": "XAUUSD",
"direction": 1,
"entry_price": 2000.0
})
signal_id = logger.log_signal({"symbol": "XAUUSD", "direction": 1, "entry_price": 2000.0})
with logger.Session() as session:
from src.core.trade_logger import ModelSignal
signal = session.get(ModelSignal, signal_id)
assert signal.created_at is not None
assert signal.updated_at is not None
assert signal.is_deleted is False
def test_constraints(logger):
from sqlalchemy.exc import IntegrityError
# Test price constraint
with pytest.raises(IntegrityError):
with logger.Session() as session:
from src.core.trade_logger import ModelSignal
bad_signal = ModelSignal(
symbol="XAUUSD",
direction=1,
entry_price=-10.0 # Should fail
)
session.add(bad_signal)
session.commit()
with pytest.raises(IntegrityError), logger.Session() as session:
from src.core.trade_logger import ModelSignal
bad_signal = ModelSignal(
symbol="XAUUSD",
direction=1,
entry_price=-10.0, # Should fail
)
session.add(bad_signal)
session.commit()