mirror of
https://github.com/triqbit/mt5-ai-xauusd-trader.git
synced 2026-11-02 06:56:15 +00:00
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:
co-authored by
saysgrok
google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
parent
281750a002
commit
32e1d448b9
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user