Skip to content

Commit f05d05f

Browse files
authored
Merge pull request #62 from AKKI0511/analyze-quanttradeai-for-new-feature-implementation
Add CLI config validation preflight command
2 parents 793fec7 + 581dbbe commit f05d05f

File tree

5 files changed

+379
-8
lines changed

5 files changed

+379
-8
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,15 @@ See docs for details: [Configuration Guide](docs/configuration.md), [Quick Refer
8282

8383
## CLI Commands
8484

85-
```bash
86-
poetry run quanttradeai fetch-data -c config/model_config.yaml # Download + cache data
87-
poetry run quanttradeai train -c config/model_config.yaml # End-to-end training pipeline
88-
poetry run quanttradeai evaluate -m <model_dir> -c config/model_config.yaml # Evaluate a saved model
89-
poetry run quanttradeai backtest -c config/backtest_config.yaml # CSV backtest (uses data_path)
85+
```bash
86+
poetry run quanttradeai fetch-data -c config/model_config.yaml # Download + cache data
87+
poetry run quanttradeai train -c config/model_config.yaml # End-to-end training pipeline
88+
poetry run quanttradeai evaluate -m <model_dir> -c config/model_config.yaml # Evaluate a saved model
89+
poetry run quanttradeai backtest -c config/backtest_config.yaml # CSV backtest (uses data_path)
9090
poetry run quanttradeai backtest-model -m <model_dir> -c config/model_config.yaml -b config/backtest_config.yaml --risk-config config/risk_config.yaml
91-
poetry run quanttradeai live-trade --url wss://example -c config/model_config.yaml
92-
```
91+
poetry run quanttradeai validate-config # Preflight validation for all YAML configs
92+
poetry run quanttradeai live-trade --url wss://example -c config/model_config.yaml
93+
```
9394

9495
## Python API
9596

@@ -273,4 +274,4 @@ Contribution guide: [CONTRIBUTING.md](CONTRIBUTING.md)
273274

274275
## License
275276

276-
MIT © Contributors — see [LICENSE](LICENSE)
277+
MIT © Contributors — see [LICENSE](LICENSE)

docs/configuration.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ The framework uses several configuration files:
1212
- **`config/impact_config.yaml`** - Market impact parameters by asset class
1313
- **`config/risk_config.yaml`** - Drawdown protection and turnover limits
1414
- **`config/position_manager.yaml`** - Live position tracking and intraday risk controls
15+
- **`config/streaming.yaml`** - Streaming providers and health monitoring settings
16+
17+
Use the CLI preflight to validate all of them at once before running training or backtests:
18+
19+
```bash
20+
poetry run quanttradeai validate-config --output-dir reports/config_validation
21+
```
22+
23+
This command loads each YAML with the same schemas used in production, then writes JSON/CSV summaries indicating which files passed and any errors found. It exits non-zero if any validation fails so you can gate CI or notebooks on clean configs.
1524

1625
## 🔧 Model Configuration
1726

quanttradeai/cli.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
run_model_backtest,
2222
)
2323
from .backtest.backtester import simulate_trades, compute_metrics
24+
from .utils.config_validator import validate_all, DEFAULT_CONFIG_PATHS
2425
import yaml
2526
import pandas as pd
2627

@@ -178,5 +179,67 @@ def cmd_live_trade(
178179
asyncio.run(run_live_pipeline(config, url))
179180

180181

182+
@app.command("validate-config")
183+
def cmd_validate_config(
184+
model_config: str = typer.Option(
185+
DEFAULT_CONFIG_PATHS["model_config"],
186+
"--model-config",
187+
help="Path to model configuration YAML",
188+
),
189+
features_config: str = typer.Option(
190+
DEFAULT_CONFIG_PATHS["features_config"],
191+
"--features-config",
192+
help="Path to features configuration YAML",
193+
),
194+
backtest_config: str = typer.Option(
195+
DEFAULT_CONFIG_PATHS["backtest_config"],
196+
"--backtest-config",
197+
help="Path to backtest configuration YAML",
198+
),
199+
impact_config: str = typer.Option(
200+
DEFAULT_CONFIG_PATHS["impact_config"],
201+
"--impact-config",
202+
help="Path to impact configuration YAML",
203+
),
204+
risk_config: str = typer.Option(
205+
DEFAULT_CONFIG_PATHS["risk_config"],
206+
"--risk-config",
207+
help="Path to risk configuration YAML",
208+
),
209+
streaming_config: str = typer.Option(
210+
DEFAULT_CONFIG_PATHS["streaming_config"],
211+
"--streaming-config",
212+
help="Path to streaming configuration YAML",
213+
),
214+
position_manager_config: str = typer.Option(
215+
DEFAULT_CONFIG_PATHS["position_manager_config"],
216+
"--position-manager-config",
217+
help="Path to position manager configuration YAML",
218+
),
219+
output_dir: str = typer.Option(
220+
"reports/config_validation",
221+
"--output-dir",
222+
help="Directory to write validation summaries",
223+
),
224+
):
225+
"""Validate all configuration files and emit a consolidated report."""
226+
227+
summary = validate_all(
228+
{
229+
"model_config": model_config,
230+
"features_config": features_config,
231+
"backtest_config": backtest_config,
232+
"impact_config": impact_config,
233+
"risk_config": risk_config,
234+
"streaming_config": streaming_config,
235+
"position_manager_config": position_manager_config,
236+
},
237+
output_dir=output_dir,
238+
)
239+
typer.echo(json.dumps(summary, indent=2))
240+
if not summary.get("all_passed", False):
241+
raise typer.Exit(code=1)
242+
243+
181244
if __name__ == "__main__":
182245
app()
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""Centralized configuration validation utilities.
2+
3+
Provides a helper to validate all first-class YAML configuration files used
4+
by QuantTradeAI and emit consolidated JSON/CSV reports. Validation reuses the
5+
project's existing Pydantic schemas and loader helpers to mirror runtime
6+
behavior.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import csv
12+
import json
13+
from dataclasses import dataclass
14+
from datetime import datetime, timezone
15+
from pathlib import Path
16+
from typing import Callable, Dict, Iterable, Mapping
17+
18+
import yaml
19+
from pydantic import ValidationError
20+
21+
from quanttradeai.utils.config_schemas import (
22+
BacktestConfigSchema,
23+
FeaturesConfigSchema,
24+
ModelConfigSchema,
25+
PositionManagerConfig,
26+
RiskManagementConfig,
27+
)
28+
from quanttradeai.utils.impact_loader import ImpactConfigError, load_impact_config
29+
30+
31+
DEFAULT_CONFIG_PATHS: Dict[str, Path] = {
32+
"model_config": Path("config/model_config.yaml"),
33+
"features_config": Path("config/features_config.yaml"),
34+
"backtest_config": Path("config/backtest_config.yaml"),
35+
"impact_config": Path("config/impact_config.yaml"),
36+
"risk_config": Path("config/risk_config.yaml"),
37+
"streaming_config": Path("config/streaming.yaml"),
38+
"position_manager_config": Path("config/position_manager.yaml"),
39+
}
40+
41+
42+
@dataclass
43+
class ValidationResult:
44+
"""Serializable validation outcome."""
45+
46+
name: str
47+
path: str
48+
passed: bool
49+
details: Dict | None = None
50+
error: str | None = None
51+
52+
def to_dict(self) -> Dict:
53+
payload = {
54+
"path": self.path,
55+
"passed": self.passed,
56+
}
57+
if self.details:
58+
payload["details"] = self.details
59+
if self.error:
60+
payload["error"] = self.error
61+
return payload
62+
63+
64+
def _load_yaml(path: Path) -> Dict:
65+
if not path.exists():
66+
raise FileNotFoundError(f"Config file not found: {path}")
67+
with path.open("r", encoding="utf-8") as f:
68+
data = yaml.safe_load(f)
69+
if data is None:
70+
raise ValueError(f"Config file is empty: {path}")
71+
if not isinstance(data, dict):
72+
raise ValueError(f"Config file must contain a mapping at root: {path}")
73+
return data
74+
75+
76+
def _validate_model_config(path: Path) -> Dict:
77+
raw = _load_yaml(path)
78+
cfg = ModelConfigSchema(**raw)
79+
return {
80+
"symbols": list(cfg.data.symbols),
81+
"timeframe": cfg.data.timeframe,
82+
"test_window": {
83+
"start": cfg.data.test_start,
84+
"end": cfg.data.test_end,
85+
},
86+
}
87+
88+
89+
def _validate_features_config(path: Path) -> Dict:
90+
raw = _load_yaml(path)
91+
cfg = FeaturesConfigSchema(**raw)
92+
steps: Iterable[str] = cfg.pipeline.steps if cfg.pipeline else []
93+
return {
94+
"pipeline_steps": list(steps),
95+
"price_features": sorted(cfg.price_features.enabled),
96+
}
97+
98+
99+
def _validate_backtest_config(path: Path) -> Dict:
100+
raw = _load_yaml(path)
101+
cfg = BacktestConfigSchema(**raw)
102+
execution = cfg.execution
103+
return {
104+
"transaction_costs": execution.transaction_costs.enabled,
105+
"slippage": execution.slippage.enabled,
106+
"impact": execution.impact.enabled,
107+
"liquidity": execution.liquidity.enabled,
108+
"borrow_fee": execution.borrow_fee.enabled,
109+
"intrabar": execution.intrabar.enabled,
110+
}
111+
112+
113+
def _validate_impact_config(path: Path) -> Dict:
114+
validated = load_impact_config(path)
115+
return {"asset_classes": sorted(validated)}
116+
117+
118+
def _validate_risk_config(path: Path) -> Dict:
119+
raw = _load_yaml(path)
120+
cfg = RiskManagementConfig(**raw.get("risk_management", raw))
121+
dd_cfg = cfg.drawdown_protection
122+
to_cfg = cfg.turnover_limits
123+
return {
124+
"drawdown_protection_enabled": dd_cfg.enabled,
125+
"turnover_limits": {
126+
"daily_max": to_cfg.daily_max,
127+
"weekly_max": to_cfg.weekly_max,
128+
"monthly_max": to_cfg.monthly_max,
129+
},
130+
}
131+
132+
133+
def _validate_streaming_config(path: Path) -> Dict:
134+
raw = _load_yaml(path)
135+
streaming_cfg = raw.get("streaming")
136+
if not isinstance(streaming_cfg, dict):
137+
raise ValueError("streaming config must include a 'streaming' mapping")
138+
providers = streaming_cfg.get("providers")
139+
provider_count = len(providers) if isinstance(providers, list) else 0
140+
return {
141+
"providers": provider_count,
142+
"symbols": streaming_cfg.get("symbols", []),
143+
}
144+
145+
146+
def _validate_position_manager_config(path: Path) -> Dict:
147+
raw = _load_yaml(path)
148+
cfg = PositionManagerConfig(**raw.get("position_manager", raw))
149+
return {
150+
"mode": cfg.mode,
151+
"reconciliation": cfg.reconciliation,
152+
"risk_management": {
153+
"drawdown_enabled": cfg.risk_management.drawdown_protection.enabled,
154+
},
155+
}
156+
157+
158+
VALIDATORS: Dict[str, Callable[[Path], Dict]] = {
159+
"model_config": _validate_model_config,
160+
"features_config": _validate_features_config,
161+
"backtest_config": _validate_backtest_config,
162+
"impact_config": _validate_impact_config,
163+
"risk_config": _validate_risk_config,
164+
"streaming_config": _validate_streaming_config,
165+
"position_manager_config": _validate_position_manager_config,
166+
}
167+
168+
169+
def _run_validator(name: str, path: Path) -> ValidationResult:
170+
validator = VALIDATORS[name]
171+
try:
172+
details = validator(path)
173+
return ValidationResult(name=name, path=str(path), passed=True, details=details)
174+
except (ValidationError, ImpactConfigError, FileNotFoundError, ValueError) as exc:
175+
return ValidationResult(name=name, path=str(path), passed=False, error=str(exc))
176+
except Exception as exc: # pragma: no cover - defensive
177+
return ValidationResult(
178+
name=name, path=str(path), passed=False, error=repr(exc)
179+
)
180+
181+
182+
def _write_reports(output_dir: Path, summary: Dict, timestamp: str) -> Dict[str, str]:
183+
output_dir.mkdir(parents=True, exist_ok=True)
184+
json_path = output_dir / f"config_validation_{timestamp}.json"
185+
with json_path.open("w", encoding="utf-8") as f:
186+
json.dump(summary, f, indent=2, default=str)
187+
188+
csv_path = output_dir / f"config_validation_{timestamp}.csv"
189+
with csv_path.open("w", encoding="utf-8", newline="") as f:
190+
writer = csv.DictWriter(f, fieldnames=["name", "path", "passed", "error"])
191+
writer.writeheader()
192+
for name, result in summary["results"].items():
193+
writer.writerow(
194+
{
195+
"name": name,
196+
"path": result["path"],
197+
"passed": result["passed"],
198+
"error": result.get("error", ""),
199+
}
200+
)
201+
202+
return {"json": str(json_path), "csv": str(csv_path)}
203+
204+
205+
def validate_all(
206+
config_paths: Mapping[str, Path | str] | None = None,
207+
*,
208+
output_dir: Path | str = "reports/config_validation",
209+
) -> Dict:
210+
"""Validate all known configuration files and persist a summary report."""
211+
212+
resolved_paths: Dict[str, Path] = {k: v for k, v in DEFAULT_CONFIG_PATHS.items()}
213+
if config_paths:
214+
for name, path in config_paths.items():
215+
if name not in VALIDATORS:
216+
continue
217+
resolved_paths[name] = Path(path)
218+
219+
results: Dict[str, Dict] = {}
220+
for name, path in resolved_paths.items():
221+
result = _run_validator(name, path)
222+
results[name] = result.to_dict()
223+
224+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
225+
summary = {
226+
"timestamp": timestamp,
227+
"all_passed": all(r["passed"] for r in results.values()),
228+
"results": results,
229+
}
230+
report_paths = _write_reports(Path(output_dir), summary, timestamp)
231+
summary["report_paths"] = report_paths
232+
return summary

0 commit comments

Comments
 (0)