Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d817c9bed | ||
|
|
17bb7545ba | ||
|
|
52c3d91639 | ||
|
|
e954883189 | ||
|
|
50025968ff | ||
|
|
6ccc066b6a |
+11
-2
@@ -19,13 +19,22 @@ config/mt5-quant.yaml
|
|||||||
config/baseline.json
|
config/baseline.json
|
||||||
config/backtest_history.json
|
config/backtest_history.json
|
||||||
|
|
||||||
# Claude Code (personal, not for repo)
|
# Agent configs (personal LLM instructions)
|
||||||
/CLAUDE.md
|
CLAUDE.md
|
||||||
.claude/
|
.claude/
|
||||||
|
.ocx
|
||||||
|
.opencode
|
||||||
|
AGENTS.md
|
||||||
|
|
||||||
# Windsurf (local workflows, not for repo)
|
# Windsurf (local workflows, not for repo)
|
||||||
.windsurf/
|
.windsurf/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Codegraph
|
||||||
|
.codegraph/
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
Vendored
-8
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"json.schemas": [
|
|
||||||
{
|
|
||||||
"fileMatch": ["server.json"],
|
|
||||||
"url": "https://raw.githubusercontent.com/modelcontextprotocol/specification/main/schema/2024-11-05/schema.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
# AGENTS.md — Developer instructions for MT5-Quant
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Open-source MCP server for MetaTrader 5 backtesting, optimization, and analytics — written entirely in Rust. Exposes 89 stdio MCP tools for compiling MQL5 EAs, running backtests via Wine, extracting deal-level data, organizing reports in SQLite, and performing 19 dimensions of analysis.
|
||||||
|
|
||||||
|
Target users: MQL developers on macOS (CrossOver or native MetaTrader5.app) and Linux (Wine/Xvfb).
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
MT5-Quant/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.rs # CLI entry + stdio MCP transport
|
||||||
|
│ ├── mcp_server.rs # McpServer struct, tool dispatch, init/notification
|
||||||
|
│ ├── mt5.rs # Wine/MT5 executable launchers
|
||||||
|
│ ├── models/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── config.rs # Config, CurrentAccount, wine detection, .set helpers
|
||||||
|
│ │ ├── deals.rs # Deal, PositionPair, DrawdownEvent
|
||||||
|
│ │ ├── metrics.rs # BacktestMetrics parsing (HTML + XML formats)
|
||||||
|
│ │ └── report.rs # ReportEntry, PipelineMetadata, BacktestJob, status
|
||||||
|
│ ├── analytics/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── extract.rs # HTML/XML report → Deal[] + metrics
|
||||||
|
│ │ └── analyze.rs # DealAnalyzer: 19+ analysis methods
|
||||||
|
│ ├── compile/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ └── mql_compiler.rs # MetaEditor compiler via Wine
|
||||||
|
│ ├── pipeline/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── backtest.rs # 5-stage pipeline: COMPILE→CLEAN→BACKTEST→EXTRACT→ANALYZE
|
||||||
|
│ │ └── stages.rs # PipelineStage enum
|
||||||
|
│ ├── optimization/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── optimizer.rs # Background optimization launcher
|
||||||
|
│ │ └── parser.rs # Optimization result XML → best params
|
||||||
|
│ ├── storage/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ └── database.rs # ReportDb: reports + deals SQLite tables, queries
|
||||||
|
│ ├── utils/ # (new) shared utilities — growing directory
|
||||||
|
│ └── tools/
|
||||||
|
│ ├── mod.rs # MCP definitions aggregation
|
||||||
|
│ ├── definitions/ # Tool schemas (JSON Schema) — 11 modules, 90 tools
|
||||||
|
│ │ ├── mod.rs # exports: all_tools(), system_tools, etc.
|
||||||
|
│ │ ├── analytics.rs # 19 analysis tools + compare_baseline
|
||||||
|
│ │ ├── backtest.rs # 7 backtest tools
|
||||||
|
│ │ ├── baseline.rs # 1 baseline tool
|
||||||
|
│ │ ├── experts.rs # 9 EA/indicator/script tools
|
||||||
|
│ │ ├── optimization.rs # 4 optimization tools
|
||||||
|
│ │ ├── reports.rs # 20 report management tools
|
||||||
|
│ │ ├── setfiles.rs # 8 .set file tools
|
||||||
|
│ │ ├── system.rs # 6 system/health tools + version update
|
||||||
|
│ │ ├── update.rs # update tool definition
|
||||||
|
│ │ └── utility.rs # 10 pre-flight/compat tools
|
||||||
|
│ └── handlers/ # Tool dispatch functions — 11 modules
|
||||||
|
│ ├── mod.rs # ToolHandler struct, dispatch table (89+ cases)
|
||||||
|
│ ├── analysis.rs # analytics/analyze dispatch
|
||||||
|
│ ├── backtest.rs # pipeline/backtest dispatch
|
||||||
|
│ ├── experts.rs # wine + WalkDir helpers: scan_mql_dir, copy_mql_to_project
|
||||||
|
│ ├── optimization.rs # optimizer dispatch
|
||||||
|
│ ├── reports.rs # ReportDb + ReportEntry dispatch
|
||||||
|
│ ├── setfiles.rs # UTF-16LE .set read/write/patch helpers
|
||||||
|
│ ├── system.rs # healthcheck, version update
|
||||||
|
│ ├── utility.rs # pre-flight, diagnostics, init_project helpers
|
||||||
|
│ └── update.rs # update tool handler
|
||||||
|
├── scripts/
|
||||||
|
│ ├── setup.sh # Auto-detect Wine/MT5, write config (1271 lines)
|
||||||
|
│ ├── platform_detect.sh # Wine path + headless detection
|
||||||
|
│ ├── build-rust.sh # cargo build --release (25 lines)
|
||||||
|
│ ├── release.sh # Version bump + changelog + tag + push (238 lines)
|
||||||
|
│ └── optimize.sh # Legacy optimization driver (pending full Rust migration)
|
||||||
|
├── analytics/ # Legacy Python (reference only — do not modify)
|
||||||
|
├── config/
|
||||||
|
│ ├── mt5-quant.example.yaml # Template user copies to mt5-quant.yaml
|
||||||
|
│ └── mt5-quant.yaml # Generated config (gitignored)
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_mcp.py # End-to-end MCP tests (stdio-driven)
|
||||||
|
│ ├── integration_test.sh # Shell-based integration tests
|
||||||
|
│ └── fixtures/ # Sample reports + CSVs for testing
|
||||||
|
├── server.json # MCP registry manifest (tracked)
|
||||||
|
├── Cargo.toml # version = 1.32.4
|
||||||
|
└── CHANGELOG.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Constraints
|
||||||
|
|
||||||
|
- **Headless/GUI**: `platform_detect.sh` auto-selects. macOS CrossOver = GUI (CrossOver abstracts display). Linux without `$DISPLAY` = Xvfb. Controlled by `display.mode` in config (`auto` | `gui` | `headless`).
|
||||||
|
- **Single MT5 instance**: Never run two backtests in parallel on the same Wine prefix.
|
||||||
|
- **Model 0 only for optimization**: Model 1 overfits martingale/grid EAs — intra-bar movement not simulated. Use `model=0` (every tick).
|
||||||
|
- **UTF-16LE .set files**: MT5 strips `||Y` flags from UTF-8. All `.set` writes use UTF-16LE + `chmod 444`. See `setfiles.rs` helpers.
|
||||||
|
- **OptMode reset**: After any optimization, `terminal.ini` gets `OptMode=-1`. Must reset to `0` before next backtest (handled in `backtest.rs` cleanup stage).
|
||||||
|
- **Report format detection**: MT5 Build 48+ writes SpreadsheetML XML (`.htm.xml`), not HTML. Both formats handled in `extract.rs` — always check for the XML variant first.
|
||||||
|
- **EA-agnostic**: Never hardcode magic numbers, strategy names, or assume specific EA behavior. All code must work with any MQL5 EA.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
1. **CLI (`main.rs`)** — parses CLi args (`--stdio`, `--port`, `--test-launch`), initializes `McpServer`, runs stdio or TCP transport loop, handles JSON-RPC requests.
|
||||||
|
|
||||||
|
2. **McpServer (`mcp_server.rs`)** — wraps `ToolHandler` with `Arc<Mutex>`, handles `initialize`/`notifications`/`tool/error` method routing. Auto-verifies Wine/MT5 on first call.
|
||||||
|
|
||||||
|
3. **ToolHandler (`tools/handlers/mod.rs`)** — dispatch table mapping 89 tool names to handler functions. Each handler module is self-contained.
|
||||||
|
|
||||||
|
4. **Definitions (`tools/definitions/`)** — JSON Schema objects for tool input/output. Never executes logic.
|
||||||
|
|
||||||
|
5. **Analytics (`analytics/`)** — two core structs:
|
||||||
|
- `ReportExtractor` — reads HTML/XML report + tester log → `Vec<Deal>` + `BacktestMetrics`
|
||||||
|
- `DealAnalyzer` — takes `Vec<Deal>` → 19 analysis methods (monthly PnL, drawdown events, streaks, etc.)
|
||||||
|
|
||||||
|
6. **Pipeline (`pipeline/backtest.rs`)** — `BacktestPipeline` struct with 5 stages:
|
||||||
|
- `COMPILE` — `MqlCompiler::compile()` via MetaEditor
|
||||||
|
- `CLEAN` — clean cache, reset OptMode
|
||||||
|
- `BACKTEST` — launch MT5 tester via Wine, poll status
|
||||||
|
- `EXTRACT` — parse HTML/XML → deals + metrics
|
||||||
|
- `ANALYZE` — run all analytics, write to SQLite
|
||||||
|
|
||||||
|
7. **Storage (`storage/database.rs`)** — `ReportDb` with two tables:
|
||||||
|
- `reports` — metrics, set file paths, verdict, tags, notes
|
||||||
|
- `deals` — individual deal rows, keyed by `report_id`
|
||||||
|
- `CREATE TABLE IF NOT EXISTS` in `init()` — idempotent on startup
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/build-rust.sh
|
||||||
|
# or
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Binary: `target/release/mt5-quant`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **Unit/integration:** `cargo test`
|
||||||
|
- **End-to-end MCP:** `python3 tests/test_mcp.py` — drives the MCP server over stdio (full backtest + all analytics)
|
||||||
|
- **Analytics only:** No MT5/Wine needed; loads deals from SQLite DB via `db.get_deals(report_id)`
|
||||||
|
- **Symbol note:** Testing account uses `XAUUSDc` (broker suffix required), not `XAUUSD`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Users copy `config/mt5-quant.example.yaml` → `config/mt5-quant.yaml` (gitignored).
|
||||||
|
Run `bash scripts/setup.sh` to auto-detect Wine, MT5 paths, architecture, and write config.
|
||||||
|
|
||||||
|
Minimum config:
|
||||||
|
```yaml
|
||||||
|
wine_executable: "/path/to/wine64"
|
||||||
|
terminal_dir: "/path/to/MetaTrader 5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
```yaml
|
||||||
|
defaults:
|
||||||
|
symbol: "XAUUSD.cent"
|
||||||
|
timeframe: "M5"
|
||||||
|
deposit: 10000
|
||||||
|
display:
|
||||||
|
mode: auto # auto | gui | headless
|
||||||
|
reports_dir: "./reports"
|
||||||
|
experts_dir: "~/.../MQL5/Experts"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing New Tools
|
||||||
|
|
||||||
|
Each new tool follows a 4-file pattern:
|
||||||
|
|
||||||
|
1. **Schema** — Add tool input/output to `tools/definitions/{domain}.rs` via `Tool::new(name) {...}` with `Input::schema` and `Output::schema`.
|
||||||
|
2. **Export** — Add to `tools/definitions/mod.rs`:
|
||||||
|
- Include in `all_tools()` array
|
||||||
|
- Add to domain exports if creating new domain
|
||||||
|
3. **Handler** — Add function in `tools/handlers/{domain}.rs`:
|
||||||
|
```rust
|
||||||
|
pub async fn handle_tool_name(config: &Config, args: &Value) -> Result<Value> {
|
||||||
|
let required = required_str(args, "param_name")?;
|
||||||
|
ok_response(json! { "result": "..." })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **Dispatch** — Add case in `handlers/mod.rs` match arm under `ToolHandler::handle()`.
|
||||||
|
|
||||||
|
### Handler Helper Functions (use instead of inline logic)
|
||||||
|
|
||||||
|
| Helper | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `required_str(args, key)` | Extract required `&str` or return error |
|
||||||
|
| `ok_response(data)` / `err_response(msg)` | Wrap in MCP content envelope |
|
||||||
|
| `resolve_report(args)` | → `(deals, metrics, report_dir)` — `report_id > report_dir > latest` |
|
||||||
|
| `prepare_analysis(args)` | → `(deals, metrics, analyzer, report_dir)` — wraps resolve_report |
|
||||||
|
| `scan_mql_dir(dir, filter, type_label)` | WalkDir with optional filter for list/search |
|
||||||
|
| `copy_mql_to_project(config, args, fallback)` | Shared copy logic for indicator/script |
|
||||||
|
|
||||||
|
Extend helpers when adding new handlers.
|
||||||
|
|
||||||
|
## Commit Style
|
||||||
|
|
||||||
|
Conventional commits: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `release:`
|
||||||
|
Keep commits focused — one logical change per commit.
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/release.sh <patch|minor|major|X.Y.Z> [--yes]
|
||||||
|
```
|
||||||
|
|
||||||
|
Script handles:
|
||||||
|
1. Bump `Cargo.toml` + `Cargo.lock` version
|
||||||
|
2. Update `server.json` (version + SHA256 placeholder)
|
||||||
|
3. Generate CHANGELOG entry from git log
|
||||||
|
4. `cargo check --quiet`
|
||||||
|
5. Commit `release: vX.Y.Z`
|
||||||
|
6. Tag `vX.Y.Z`
|
||||||
|
7. Push — CI (`release.yml`) triggered:
|
||||||
|
- Build macOS arm64 + Linux x64 binaries
|
||||||
|
- Publish to crates.io
|
||||||
|
- Create GitHub Release with artifacts
|
||||||
|
- Build MCP package, compute SHA256
|
||||||
|
- Update `server.json` SHA256, push
|
||||||
|
- Publish to MCP Registry
|
||||||
|
|
||||||
|
## Deal Storage
|
||||||
|
|
||||||
|
- Deals stored in `deals` SQLite table, keyed by `report_id`. **No `deals.csv` written automatically.**
|
||||||
|
- `db.insert_deals(report_id, &[Deal])` called in pipeline after extraction.
|
||||||
|
- `db.get_deals(report_id)` loads deals for analytics. `db.get_by_report_dir(path)` resolves by filesystem path.
|
||||||
|
- `export_deals_csv` tool writes CSV on demand.
|
||||||
|
|
||||||
|
## DB Schema Evolution
|
||||||
|
|
||||||
|
Add new tables/columns in `ReportDb::init()` using `CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS` — runs on every startup, is idempotent. No migration files needed.
|
||||||
|
|
||||||
|
## Wine / MT5 Notes
|
||||||
|
|
||||||
|
- Wine executable paths differ across platforms:
|
||||||
|
- macOS MT5.app: `/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine64`
|
||||||
|
- macOS CrossOver: `/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine64`
|
||||||
|
- Linux: `/usr/bin/wine64` (or first `wine64` on PATH)
|
||||||
|
- MT5 terminal_dir is auto-resolved from wine_executable path.
|
||||||
|
- Apple Silicon: append `arch -x86_64` prefix for x86 Wine binaries (auto-detected).
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- **M1/Memory issues**: Wine on Apple Silicon runs x86 via Rosetta 2. Watch memory pressure.
|
||||||
|
- **Model mismatch**: Grid/martingale EAs need `model=0` (every tick). Model 1/2 will produce zero deals.
|
||||||
|
- **Set file encoding**: Always use UTF-16LE with `chmod 444`. UTF-8 strips `||Y` flags.
|
||||||
|
- **OptMode=-1**: Must clear after optimization. Handled in pipeline cleanup stage.
|
||||||
|
- **Report not found**: Check `<terminal_dir>/history/` for correct symbol name (broker suffix matters: `XAUUSDc` vs `XAUUSD`).
|
||||||
|
- **Journal-only backtest**: `ShutdownTerminal=1` doesn't exit MT5 on Wine. Inactivity watchdog (in `launch_backtest`) handles this with `inactivity_kill_secs` param.
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.33.0] — 2026-06-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`src/utils/` module**: shared `read_file_as_utf8` utility for UTF-16LE BOM detection, used across set file and config handlers.
|
||||||
|
- **OS detection and diagnostics**: `utility.rs` gains `OsType` enum, `get_os_context`, `get_system_memory`, and `crossover_server_running` helper for better CrossOver diagnostics.
|
||||||
|
- **Wine binary detection**: MT5.app and CrossOver now detect both `wine` and `wine64` binaries; Homebrew and Linux paths add `wine` alongside `wine64`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Optimizer launch mechanism**: rewritten from `/mt5mcp_backtest.ini` + `.bat` to `/config:` INI + shell script. Now patches `terminal.ini` directly with full optimization parameters and appends agent configuration — no longer relies on batch file or separate INI.
|
||||||
|
- **OptMode reset**: removed as a separate step; now included inline in `terminal.ini` patching during optimizer startup.
|
||||||
|
- **Wine prefix resolution**: changed from 2-parent to 3-parent traversal, matching the backtest pipeline's Wine prefix logic.
|
||||||
|
- **Set file parsing**: parameter delimiter changed from `:` to `=` (matches actual MT5 .set format); `||Y` sweep param parsing rewritten to split on `||` for accurate range extraction.
|
||||||
|
- **OptimizationParams**: `model` parameter removed (was hardcoded to `0` everywhere anyway; MT5 now defaults correctly).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Set file cross-platform reading**: `.set` files are now read as UTF-16LE (with BOM) or UTF-8 regardless of platform, fixing potential encoding issues on macOS/Linux.
|
||||||
|
- **Read-only set file overwrite**: handles pre-existing read-only files during `.set` write by removing them first.
|
||||||
|
|
||||||
|
|
||||||
## [1.32.4] — 2026-04-25
|
## [1.32.4] — 2026-04-25
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mt5-quant"
|
name = "mt5-quant"
|
||||||
version = "1.32.4"
|
version = "1.33.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "MCP server for MT5 strategy development on macOS/Linux"
|
description = "MCP server for MT5 strategy development on macOS/Linux"
|
||||||
authors = ["masdevid <masdevid@example.com>"]
|
authors = ["masdevid <masdevid@example.com>"]
|
||||||
|
|||||||
@@ -27,83 +27,33 @@ Claude: [compile → clean → backtest → analyze 1,847 deals]
|
|||||||
|
|
||||||
**Others** typically run on Windows using the [MetaTrader5 Python package](https://pypi.org/project/MetaTrader5/), providing full terminal operations. MT5-Quant fills the gap: **organizing backtest reports, extracting deal-level insights, and managing optimization workflows** — none of which MT5 or its Python API expose natively.
|
**Others** typically run on Windows using the [MetaTrader5 Python package](https://pypi.org/project/MetaTrader5/), providing full terminal operations. MT5-Quant fills the gap: **organizing backtest reports, extracting deal-level insights, and managing optimization workflows** — none of which MT5 or its Python API expose natively.
|
||||||
|
|
||||||
## Why Rust
|
|
||||||
|
|
||||||
**MT5-Quant is built entirely in Rust** — one static binary, zero dependencies, instant startup.
|
|
||||||
|
|
||||||
| | Rust | Python/Node |
|
|
||||||
|---|---|---|
|
|
||||||
| **Startup** | ~10ms | 200–500ms |
|
|
||||||
| **Deploy** | Single binary | venv + pip/npm |
|
|
||||||
| **Memory** | <50MB, no GC pauses | Unpredictable spikes |
|
|
||||||
| **Safety** | Compile-time guaranteed | Runtime exceptions |
|
|
||||||
|
|
||||||
**Why this matters for trading:**
|
|
||||||
- **No garbage collection** — Process 100k+ deal rows without GC pauses during live analysis
|
|
||||||
- **Async via Tokio** — Handle multiple tool calls concurrently (poll status while streaming logs)
|
|
||||||
- **Cross-platform** — Same source compiles to native macOS arm64 and Linux x86_64
|
|
||||||
- **Type-safe pipelines** — MQL5 compilation, Wine path handling, SQLite queries: all checked at compile time
|
|
||||||
|
|
||||||
## Quick Install
|
|
||||||
|
|
||||||
### Option 1: Pre-built Binary (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-macos-arm64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
bash scripts/setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Cargo Install (if you have Rust)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install mt5-quant
|
|
||||||
mt5-quant --help
|
|
||||||
```
|
|
||||||
|
|
||||||
Compiles from source. Takes 2–5 minutes. Requires Rust 1.70+.
|
|
||||||
|
|
||||||
### Register MCP Server
|
|
||||||
|
|
||||||
| Platform | Command / Config | Docs |
|
|
||||||
|----------|------------------|------|
|
|
||||||
| **Claude Code** | `claude mcp add mt5-quant -- $(pwd)/mt5-quant` | [Setup →](docs/QUICKSTART.md) |
|
|
||||||
| **Windsurf** | Edit `~/.codeium/windsurf/mcp_config.json` | [WINDSURF.md →](docs/WINDSURF.md) |
|
|
||||||
| **Cursor** | Edit `~/.cursor/mcp.json` or use Settings → MCP | [CURSOR.md →](docs/CURSOR.md) |
|
|
||||||
| **VS Code** | Edit `.vscode/mcp.json` or run `MCP: Add Server` | [VSCODE.md →](docs/VSCODE.md) |
|
|
||||||
| **Claude Desktop** | Agent Panel → ... → MCP Servers → Edit configuration | [CLAUDE.md →](docs/CLAUDE.md) |
|
|
||||||
|
|
||||||
> **Note:** Use absolute paths like `/Users/name/mt5-quant/mt5-quant` or `$(pwd)/mt5-quant`, not relative paths like `./mt5-quant`.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
### Install MT5-Quant
|
||||||
|
|
||||||
|
Ask your LLM coding platform to install and configure MT5-Quant:
|
||||||
|
|
||||||
|
> "Please install mt5-quant. It's a Rust-based MCP server for MT5 backtesting at https://github.com/masdevid/mt5-quant. Run its `scripts/setup.sh` to auto-detect Wine and MT5 paths, register the MCP server, and configure everything."
|
||||||
|
|
||||||
|
The LLM will:
|
||||||
|
1. Download the pre-built binary (or `cargo build --release`)
|
||||||
|
2. Run `scripts/setup.sh` to auto-detect Wine/MT5 and write config
|
||||||
|
3. Register the MCP server with your coding platform
|
||||||
|
|
||||||
|
### First Backtest
|
||||||
|
|
||||||
```
|
```
|
||||||
Run a backtest on MyEA from 2025.01.01 to 2025.03.31
|
Run a backtest on MyEA from 2025.01.01 to 2025.03.31
|
||||||
```
|
```
|
||||||
|
|
||||||
The AI runs the full pipeline: compile → clean cache → backtest → extract → analyze.
|
The AI runs the full pipeline: compile → clean cache → backtest → extract → analyze.
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
| Doc | Purpose |
|
|
||||||
|-----|---------|
|
|
||||||
| [QUICKSTART.md](docs/QUICKSTART.md) | Complete setup for macOS/Linux |
|
|
||||||
| [WINDSURF.md](docs/WINDSURF.md) | Windsurf IDE setup |
|
|
||||||
| [CURSOR.md](docs/CURSOR.md) | Cursor IDE setup |
|
|
||||||
| [VSCODE.md](docs/VSCODE.md) | VS Code setup |
|
|
||||||
| [CLAUDE.md](docs/CLAUDE.md) | Claude Desktop setup |
|
|
||||||
| [CONFIG.md](docs/CONFIG.md) | Configuration reference |
|
|
||||||
| [TOOLS.md](docs/MCP_TOOLS.md) | All 89 tools documented |
|
|
||||||
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design and internals |
|
|
||||||
| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common issues |
|
|
||||||
| [REMOTE_AGENTS.md](docs/REMOTE_AGENTS.md) | Linux optimization agents |
|
|
||||||
|
|
||||||
## MCP Tools (89)
|
## MCP Tools (89)
|
||||||
|
|
||||||
### Core workflow
|
### Core workflow
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `run_backtest` | Full pipeline: compile → clean → backtest → extract → analyze |
|
| `run_backtest` | Full pipeline: compile → clean → backtest → extract → analyze |
|
||||||
| `run_backtest_quick` | Quick backtest using pre-compiled EA (skip compile) |
|
| `run_backtest_quick` | Quick backtest using pre-compiled EA (skip compile) |
|
||||||
| `run_backtest_only` | Backtest only - just extract raw trades, no analysis |
|
| `run_backtest_only` | Backtest only - just extract raw trades, no analysis |
|
||||||
@@ -124,7 +74,7 @@ The AI runs the full pipeline: compile → clean cache → backtest → extract
|
|||||||
### Granular Analytics (individual analysis)
|
### Granular Analytics (individual analysis)
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `analyze_monthly_pnl` | Monthly P/L breakdown only |
|
| `analyze_monthly_pnl` | Monthly P/L breakdown only |
|
||||||
| `analyze_drawdown_events` | Drawdown events and causes only |
|
| `analyze_drawdown_events` | Drawdown events and causes only |
|
||||||
| `analyze_top_losses` | Worst losing deals only |
|
| `analyze_top_losses` | Worst losing deals only |
|
||||||
@@ -139,7 +89,7 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### Deal-Level Analytics (New)
|
### Deal-Level Analytics (New)
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `list_deals` | List individual deals with filters (type, profit range, volume, dates) |
|
| `list_deals` | List individual deals with filters (type, profit range, volume, dates) |
|
||||||
| `search_deals_by_comment` | Full-text search in deal comments (e.g., "Layer #3") |
|
| `search_deals_by_comment` | Full-text search in deal comments (e.g., "Layer #3") |
|
||||||
| `search_deals_by_magic` | Filter deals by EA magic number |
|
| `search_deals_by_magic` | Filter deals by EA magic number |
|
||||||
@@ -154,7 +104,7 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### Monitoring
|
### Monitoring
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `verify_setup` | Check Wine/MT5 paths, Wine version, and EA/set file counts |
|
| `verify_setup` | Check Wine/MT5 paths, Wine version, and EA/set file counts |
|
||||||
| `get_optimization_status` | Check live state of a background optimization job |
|
| `get_optimization_status` | Check live state of a background optimization job |
|
||||||
| `list_jobs` | All optimization jobs with compact status in one call |
|
| `list_jobs` | All optimization jobs with compact status in one call |
|
||||||
@@ -162,7 +112,7 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### Reports & logs
|
### Reports & logs
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `list_reports` | Compact table of all runs with key metrics — no full analysis needed |
|
| `list_reports` | Compact table of all runs with key metrics — no full analysis needed |
|
||||||
| `get_latest_report` | Get most recent report with optional equity chart |
|
| `get_latest_report` | Get most recent report with optional equity chart |
|
||||||
| `search_reports` | Find reports by EA, symbol, date range, or profit criteria |
|
| `search_reports` | Find reports by EA, symbol, date range, or profit criteria |
|
||||||
@@ -181,7 +131,7 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### History & baseline
|
### History & baseline
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `archive_report` | Convert one report dir → compact JSON entry in `backtest_history.json`, optionally delete source |
|
| `archive_report` | Convert one report dir → compact JSON entry in `backtest_history.json`, optionally delete source |
|
||||||
| `archive_all_reports` | Bulk-archive all report dirs then optionally delete them; keeps N newest safe |
|
| `archive_all_reports` | Bulk-archive all report dirs then optionally delete them; keeps N newest safe |
|
||||||
| `get_history` | Query history with filters (EA, symbol, verdict, profit, DD) and sort options |
|
| `get_history` | Query history with filters (EA, symbol, verdict, profit, DD) and sort options |
|
||||||
@@ -190,14 +140,14 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### Cache management
|
### Cache management
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `cache_status` | MT5 tester cache size breakdown by symbol — check before cleaning |
|
| `cache_status` | MT5 tester cache size breakdown by symbol — check before cleaning |
|
||||||
| `clean_cache` | Delete tester cache files; supports per-symbol and `dry_run` |
|
| `clean_cache` | Delete tester cache files; supports per-symbol and `dry_run` |
|
||||||
|
|
||||||
### Pre-flight & Validation
|
### Pre-flight & Validation
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `get_active_account` | Get current MT5 account session (login, server, available symbols) |
|
| `get_active_account` | Get current MT5 account session (login, server, available symbols) |
|
||||||
| `check_symbol_data_status` | Validate symbol has sufficient history data for date range |
|
| `check_symbol_data_status` | Validate symbol has sufficient history data for date range |
|
||||||
| `check_mt5_status` | Check if MT5 terminal is installed and ready |
|
| `check_mt5_status` | Check if MT5 terminal is installed and ready |
|
||||||
@@ -206,7 +156,7 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### Debugging & Diagnostics (New)
|
### Debugging & Diagnostics (New)
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `diagnose_wine` | Check Wine installation, version, and prefix health |
|
| `diagnose_wine` | Check Wine installation, version, and prefix health |
|
||||||
| `get_mt5_logs` | Get MT5 terminal, tester, or MetaEditor logs with filtering |
|
| `get_mt5_logs` | Get MT5 terminal, tester, or MetaEditor logs with filtering |
|
||||||
| `search_mt5_errors` | Search logs for error patterns (crash, exception, access violation) |
|
| `search_mt5_errors` | Search logs for error patterns (crash, exception, access violation) |
|
||||||
@@ -222,7 +172,7 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### Project Management
|
### Project Management
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `init_project` | Scaffold new MQL5 project with templates (scalper/swing/grid/basic) |
|
| `init_project` | Scaffold new MQL5 project with templates (scalper/swing/grid/basic) |
|
||||||
| `create_set_template` | Generate .set parameter file from EA input variables |
|
| `create_set_template` | Generate .set parameter file from EA input variables |
|
||||||
| `export_report` | Export backtest report to CSV, JSON, or Markdown |
|
| `export_report` | Export backtest report to CSV, JSON, or Markdown |
|
||||||
@@ -230,14 +180,14 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### History & Comparison
|
### History & Comparison
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `get_backtest_history` | List all backtests for EA/symbol with summary metrics |
|
| `get_backtest_history` | List all backtests for EA/symbol with summary metrics |
|
||||||
| `compare_backtests` | Compare 2+ backtest results side-by-side with analysis |
|
| `compare_backtests` | Compare 2+ backtest results side-by-side with analysis |
|
||||||
|
|
||||||
### .set file — read / write
|
### .set file — read / write
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `list_set_files` | All .set files in tester profiles dir with sweep stats and combination counts |
|
| `list_set_files` | All .set files in tester profiles dir with sweep stats and combination counts |
|
||||||
| `read_set_file` | Parse UTF-16LE `.set` file → structured JSON params |
|
| `read_set_file` | Parse UTF-16LE `.set` file → structured JSON params |
|
||||||
| `write_set_file` | Write full params dict → UTF-16LE `.set` with `chmod 444` |
|
| `write_set_file` | Write full params dict → UTF-16LE `.set` with `chmod 444` |
|
||||||
@@ -247,7 +197,7 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### .set file — analysis & generation
|
### .set file — analysis & generation
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `describe_sweep` | Swept params, value counts, and total optimization combinations |
|
| `describe_sweep` | Swept params, value counts, and total optimization combinations |
|
||||||
| `diff_set_files` | Side-by-side diff of two `.set` files — only changed params returned |
|
| `diff_set_files` | Side-by-side diff of two `.set` files — only changed params returned |
|
||||||
| `set_from_optimization` | Generate a clean backtest `.set` from `get_optimization_results` params; optionally narrow sweep |
|
| `set_from_optimization` | Generate a clean backtest `.set` from `get_optimization_results` params; optionally narrow sweep |
|
||||||
@@ -255,7 +205,7 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
### Search & Discovery
|
### Search & Discovery
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-----|
|
||||||
| `search_experts` | Search EAs by name pattern across all directories |
|
| `search_experts` | Search EAs by name pattern across all directories |
|
||||||
| `search_indicators` | Search indicators by name pattern |
|
| `search_indicators` | Search indicators by name pattern |
|
||||||
| `search_scripts` | Search scripts by name pattern |
|
| `search_scripts` | Search scripts by name pattern |
|
||||||
@@ -266,7 +216,7 @@ Full schema: [docs/MCP_TOOLS.md](docs/MCP_TOOLS.md)
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
Run `verify_setup` from Claude first — it checks all paths and returns actionable hints.
|
Run `verify_setup` from your LLM first — it checks all paths and returns actionable hints.
|
||||||
|
|
||||||
For crashes or unexplained failures during backtest/compile/optimization:
|
For crashes or unexplained failures during backtest/compile/optimization:
|
||||||
- `diagnose_wine` — Check Wine installation and prefix health
|
- `diagnose_wine` — Check Wine installation and prefix health
|
||||||
|
|||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
# Claude Desktop MCP Integration Setup
|
|
||||||
|
|
||||||
## Quick Setup
|
|
||||||
|
|
||||||
### Option 1: Install via MCP Registry (Recommended)
|
|
||||||
|
|
||||||
Search for `mt5-quant` in Claude Desktop's MCP manager, or add directly to your config.
|
|
||||||
|
|
||||||
### Option 2: Download Prebuilt Binary
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# macOS (Apple Silicon)
|
|
||||||
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-macos-arm64.tar.gz
|
|
||||||
tar -xzf mt5-quant.tar.gz
|
|
||||||
|
|
||||||
# Linux (x64)
|
|
||||||
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-linux-x64.tar.gz
|
|
||||||
tar -xzf mt5-quant.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 3: Build from Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/masdevid/mt5-quant
|
|
||||||
cd mt5-quant
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configure Claude Desktop
|
|
||||||
|
|
||||||
### Step 1: Open MCP Configuration
|
|
||||||
|
|
||||||
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows).
|
|
||||||
|
|
||||||
### Step 2: Add mt5-quant
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/absolute/path/to/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Reload and Verify
|
|
||||||
|
|
||||||
1. Save the file
|
|
||||||
2. **Restart Claude Desktop**
|
|
||||||
3. In a new conversation, type: `What tools do you have access to?`
|
|
||||||
4. The agent should list MT5 tools like `verify_setup`, `run_backtest`, etc.
|
|
||||||
|
|
||||||
## Full Example Configuration
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/Users/name/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
},
|
|
||||||
"github": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
||||||
"env": {
|
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Path notes:**
|
|
||||||
- Prebuilt binary: `/Users/name/mt5-quant/mcp-server/bin/mt5-quant` (extracted from release tarball)
|
|
||||||
- Dev build: `/Users/name/mt5-quant/target/release/mt5-quant` (after `cargo build --release`)
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Claude Desktop supports `${env:VAR_NAME}` syntax for environment variable substitution:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/Users/name/mt5-quant/mcp-server/bin/mt5-quant",
|
|
||||||
"env": {
|
|
||||||
"MT5_MCP_HOME": "${env:HOME}/.config/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verify Setup
|
|
||||||
|
|
||||||
In a Claude Desktop conversation, type:
|
|
||||||
|
|
||||||
```
|
|
||||||
Run verify_setup
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output:
|
|
||||||
```
|
|
||||||
Wine: /Applications/MetaTrader 5.app/.../wine64
|
|
||||||
MT5 dir: ~/Library/Application Support/.../MetaTrader 5
|
|
||||||
Display: gui
|
|
||||||
Arch: arch -x86_64
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "Tool not found" or server not appearing
|
|
||||||
|
|
||||||
1. Double-check the absolute path in `claude_desktop_config.json`
|
|
||||||
2. Ensure the binary has execute permissions: `chmod +x /path/to/mt5-quant`
|
|
||||||
3. Restart Claude Desktop completely
|
|
||||||
4. Check Claude Desktop logs: `~/Library/Logs/Claude/` (macOS)
|
|
||||||
|
|
||||||
### JSON syntax errors
|
|
||||||
|
|
||||||
1. Validate the JSON at [jsonlint.com](https://jsonlint.com)
|
|
||||||
2. Ensure no trailing commas
|
|
||||||
3. Use absolute paths (not `~` or relative paths)
|
|
||||||
|
|
||||||
### Agent hallucinating tool parameters
|
|
||||||
|
|
||||||
1. Verify the tool is available: "List your available MCP tools"
|
|
||||||
2. Start a new conversation
|
|
||||||
3. Be explicit: "Use the `run_backtest` tool with expert=MyEA"
|
|
||||||
|
|
||||||
## Configuration Location
|
|
||||||
|
|
||||||
| Platform | Path |
|
|
||||||
|----------|------|
|
|
||||||
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
||||||
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Claude Desktop MCP Documentation](https://docs.anthropic.com/en/docs/claude-code/mcp)
|
|
||||||
- [MCP Server Reference](https://github.com/modelcontextprotocol/servers)
|
|
||||||
- [MT5-Quant Tools Reference](./MCP_TOOLS.md)
|
|
||||||
+4
-51
@@ -1,18 +1,15 @@
|
|||||||
# Configuration Reference
|
# Configuration Reference
|
||||||
|
|
||||||
## Config File Location
|
## Config File
|
||||||
|
|
||||||
```
|
```
|
||||||
config/mt5-quant.yaml # Project directory (development)
|
config/mt5-quant.yaml # Project directory (development)
|
||||||
~/.config/mt5-quant/config/mt5-quant.yaml # System-wide (production)
|
~/.config/mt5-quant/config/mt5-quant.yaml # System-wide (production)
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment variable to override:
|
Override with env: `export MT5_MCP_HOME=/path/to/config`
|
||||||
```bash
|
|
||||||
export MT5_MCP_HOME=/path/to/config
|
|
||||||
```
|
|
||||||
|
|
||||||
## Full Config Example
|
## Full Example
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Required: Wine executable path
|
# Required: Wine executable path
|
||||||
@@ -52,53 +49,9 @@ optimization:
|
|||||||
min_agents: 4
|
min_agents: 4
|
||||||
```
|
```
|
||||||
|
|
||||||
## Platform-Specific Examples
|
|
||||||
|
|
||||||
### macOS with MetaTrader 5.app
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
wine_executable: "/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine64"
|
|
||||||
terminal_dir: "~/Library/Application Support/net.metaquotes.wine.metatrader5/drive_c/Program Files/MetaTrader 5"
|
|
||||||
defaults:
|
|
||||||
symbol: "XAUUSDc"
|
|
||||||
timeframe: "M5"
|
|
||||||
deposit: 10000
|
|
||||||
```
|
|
||||||
|
|
||||||
### macOS with CrossOver
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
wine_executable: "/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine64"
|
|
||||||
terminal_dir: "~/Library/Application Support/MetaQuotes/Terminal/<hash>/drive_c/Program Files/MetaTrader 5"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
wine_executable: "/usr/bin/wine64"
|
|
||||||
terminal_dir: "~/.wine/drive_c/Program Files/MetaTrader 5"
|
|
||||||
display:
|
|
||||||
mode: headless
|
|
||||||
xvfb_display: ":99"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Headless Mode (Linux VPS)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
display:
|
|
||||||
mode: headless
|
|
||||||
xvfb_display: ":99"
|
|
||||||
xvfb_screen: "1024x768x16"
|
|
||||||
```
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
```bash
|
|
||||||
sudo apt install xvfb
|
|
||||||
```
|
|
||||||
|
|
||||||
## Auto-Detection
|
## Auto-Detection
|
||||||
|
|
||||||
`setup.sh` automatically detects:
|
`setup.sh` detects:
|
||||||
- Wine executable (MetaTrader 5.app, CrossOver, or system Wine)
|
- Wine executable (MetaTrader 5.app, CrossOver, or system Wine)
|
||||||
- MT5 terminal directory
|
- MT5 terminal directory
|
||||||
- Architecture (Apple Silicon adds `arch -x86_64`)
|
- Architecture (Apple Silicon adds `arch -x86_64`)
|
||||||
|
|||||||
-129
@@ -1,129 +0,0 @@
|
|||||||
# Cursor MCP Integration Setup
|
|
||||||
|
|
||||||
## Quick Setup
|
|
||||||
|
|
||||||
### Option 1: Download Prebuilt Binary (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# macOS (Apple Silicon)
|
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-macos-arm64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
|
|
||||||
# Linux (x64)
|
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-linux-x64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Build from Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configure Cursor
|
|
||||||
|
|
||||||
### Method 1: Settings UI (Recommended)
|
|
||||||
|
|
||||||
1. Open Cursor Settings (`Cmd/Ctrl + ,`)
|
|
||||||
2. Navigate to **Features** → **MCP**
|
|
||||||
3. Click **Add Custom MCP**
|
|
||||||
4. Enter:
|
|
||||||
- **Name**: `mt5-quant`
|
|
||||||
- **Command**: `/path/to/mt5-quant/mcp-server/bin/mt5-quant`
|
|
||||||
- **Type**: `stdio`
|
|
||||||
|
|
||||||
### Method 2: Edit mcp.json Directly
|
|
||||||
|
|
||||||
Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the file if it doesn't exist:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/.cursor
|
|
||||||
cat > ~/.cursor/mcp.json << 'EOF'
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verify Setup
|
|
||||||
|
|
||||||
In Cursor chat, type:
|
|
||||||
|
|
||||||
```
|
|
||||||
Run verify_setup
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output:
|
|
||||||
```
|
|
||||||
Wine: /Applications/MetaTrader 5.app/.../wine64
|
|
||||||
MT5 dir: ~/Library/Application Support/.../MetaTrader 5
|
|
||||||
Display: gui
|
|
||||||
Arch: arch -x86_64
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Locations
|
|
||||||
|
|
||||||
| Scope | Path | Use Case |
|
|
||||||
|-------|------|----------|
|
|
||||||
| Global | `~/.cursor/mcp.json` | Available in all projects |
|
|
||||||
| Project | `.cursor/mcp.json` | Project-specific tools |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### MCP server not appearing
|
|
||||||
|
|
||||||
1. Check MCP panel in Cursor Settings
|
|
||||||
2. Verify the path is absolute (not relative)
|
|
||||||
3. Test binary: `/path/to/mt5-quant/mcp-server/bin/mt5-quant --help`
|
|
||||||
4. View MCP logs: Output panel → select "MCP" from dropdown
|
|
||||||
|
|
||||||
### Config interpolation
|
|
||||||
|
|
||||||
Cursor supports variable substitution in `mcp.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "${userHome}/mt5-quant/mcp-server/bin/mt5-quant",
|
|
||||||
"args": ["--config", "${workspaceFolder}/config.yaml"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Available variables:
|
|
||||||
- `${userHome}` - Home directory
|
|
||||||
- `${workspaceFolder}` - Project root
|
|
||||||
- `${workspaceFolderBasename}` - Project folder name
|
|
||||||
- `${env:VAR_NAME}` - Environment variable
|
|
||||||
|
|
||||||
### Tool not found errors
|
|
||||||
|
|
||||||
If the agent says "Tool not found":
|
|
||||||
1. Check the server is enabled in MCP settings
|
|
||||||
2. Try disabling and re-enabling the server
|
|
||||||
3. Restart Cursor
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Cursor MCP Documentation](https://cursor.com/docs/context/mcp)
|
|
||||||
- [MT5-Quant Tools Reference](./MCP_TOOLS.md)
|
|
||||||
+18
-166
@@ -1,192 +1,44 @@
|
|||||||
# Quickstart Guide
|
# Quickstart Guide
|
||||||
|
|
||||||
## 1. Download or Build
|
For end-users wanting full platform-specific steps, see the [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
|
||||||
|
|
||||||
### Option A: Prebuilt Binary (Recommended)
|
## Install
|
||||||
|
|
||||||
```bash
|
Ask your LLM platform to install MT5-Quant:
|
||||||
# macOS (Apple Silicon)
|
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-macos-arm64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
|
|
||||||
# Linux (x64)
|
> "Please install mt5-quant from https://github.com/masdevid/mt5-quant. Download the pre-built binary, run `scripts/setup.sh` to auto-detect Wine/MT5 paths, and register the MCP server."
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-linux-x64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option B: Build from Source
|
The LLM will handle:
|
||||||
|
1. Download binary or `cargo build --release`
|
||||||
|
2. Run setup script to auto-detect Wine and MT5 paths
|
||||||
|
3. Write `config/mt5-quant.yaml` (gitignored)
|
||||||
|
4. Register MCP server with your platform
|
||||||
|
|
||||||
```bash
|
## Minimal Config
|
||||||
git clone https://github.com/masdevid/mt5-quant
|
|
||||||
cd mt5-quant
|
|
||||||
bash scripts/build-rust.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Install MetaTrader 5
|
If manual config is needed, `config/mt5-quant.yaml` requires only:
|
||||||
|
|
||||||
### macOS - MetaTrader 5.app (Free)
|
|
||||||
|
|
||||||
1. Download from [metatrader5.com](https://www.metatrader5.com/en/download)
|
|
||||||
2. Install to `/Applications`
|
|
||||||
3. **Launch once** to initialize Wine prefix (~30s), then quit
|
|
||||||
|
|
||||||
Auto-detected paths:
|
|
||||||
- Wine: `/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine64`
|
|
||||||
- MT5: `~/Library/Application Support/net.metaquotes.wine.metatrader5/drive_c/Program Files/MetaTrader 5`
|
|
||||||
|
|
||||||
### macOS - CrossOver (Paid, Better Compatibility)
|
|
||||||
|
|
||||||
1. Install [CrossOver](https://www.codeweavers.com/)
|
|
||||||
2. Create bottle `MetaTrader5`
|
|
||||||
3. Install MT5 inside bottle
|
|
||||||
|
|
||||||
Auto-detected paths:
|
|
||||||
- Wine: `/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine64`
|
|
||||||
- MT5: `~/Library/Application Support/MetaQuotes/<hash>/drive_c/Program Files/MetaTrader 5`
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Debian/Ubuntu
|
|
||||||
sudo apt install wine64 xvfb
|
|
||||||
|
|
||||||
# Fedora/RHEL
|
|
||||||
sudo dnf install wine xorg-x11-server-Xvfb
|
|
||||||
|
|
||||||
# Install MT5
|
|
||||||
wine64 MetaTrader5Setup.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
MT5 location: `~/.wine/drive_c/Program Files/MetaTrader 5`
|
|
||||||
|
|
||||||
## 3. Configure
|
|
||||||
|
|
||||||
Run the setup script to auto-detect paths:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/setup.sh # interactive
|
|
||||||
bash scripts/setup.sh --yes # non-interactive (CI)
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates `config/mt5-quant.yaml` (gitignored).
|
|
||||||
|
|
||||||
Minimum config:
|
|
||||||
```yaml
|
```yaml
|
||||||
wine_executable: "/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine64"
|
wine_executable: "/path/to/wine64"
|
||||||
terminal_dir: "~/Library/Application Support/net.metaquotes.wine.metatrader5/drive_c/Program Files/MetaTrader 5"
|
terminal_dir: "/path/to/MetaTrader 5"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Register MCP Server
|
`setup.sh` auto-detects both.
|
||||||
|
|
||||||
### Claude Code
|
## Verify
|
||||||
|
|
||||||
```bash
|
|
||||||
claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant
|
|
||||||
# Or: claude mcp add mt5-quant -- $(pwd)/mcp-server/bin/mt5-quant
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
```bash
|
|
||||||
claude mcp list
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windsurf
|
|
||||||
|
|
||||||
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cursor
|
|
||||||
|
|
||||||
Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use Settings → MCP → Add Custom MCP.
|
|
||||||
|
|
||||||
### VS Code
|
|
||||||
|
|
||||||
Add to `.vscode/mcp.json` in your workspace:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"servers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or run `MCP: Add Server` from Command Palette.
|
|
||||||
|
|
||||||
### Claude Desktop
|
|
||||||
|
|
||||||
1. Open Agent panel → Click "..." menu → MCP Servers
|
|
||||||
2. Click "Manage MCP Servers"
|
|
||||||
3. Click "View raw config" or "Edit configuration"
|
|
||||||
4. Add to `mcp_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/path/to/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
5. Restart Claude Desktop to apply changes
|
|
||||||
|
|
||||||
## 5. Verify Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/platform_detect.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Or in Claude/Windsurf:
|
|
||||||
```
|
```
|
||||||
Run verify_setup
|
Run verify_setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected output:
|
## Run Backtest
|
||||||
```
|
|
||||||
Wine: /Applications/MetaTrader 5.app/.../wine64
|
|
||||||
MT5 dir: ~/Library/Application Support/.../MetaTrader 5
|
|
||||||
Display: gui
|
|
||||||
Arch: arch -x86_64
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Run First Backtest
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Run a backtest on MyEA from 2025.01.01 to 2025.03.31
|
Run a backtest on MyEA from 2025.01.01 to 2025.03.31
|
||||||
```
|
```
|
||||||
|
|
||||||
The AI will:
|
The AI runs: compile → clean → backtest → extract → analyze.
|
||||||
1. Verify setup
|
|
||||||
2. Compile your EA
|
|
||||||
3. Clean MT5 cache
|
|
||||||
4. Run backtest
|
|
||||||
5. Extract and analyze results
|
|
||||||
6. Report key findings
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Next:** See [TOOLS.md](TOOLS.md) for all 89 available tools.
|
**Next:** See [TOOLS.md](docs/MCP_TOOLS.md) for all 89 tools.
|
||||||
|
|||||||
+14
-95
@@ -11,14 +11,12 @@ MT5's distributed testing lets you farm optimization work across multiple machin
|
|||||||
**Mac (master)**
|
**Mac (master)**
|
||||||
- MetaTrader 5 installed via CrossOver or Wine
|
- MetaTrader 5 installed via CrossOver or Wine
|
||||||
- MT5-Quant configured and working for local backtests
|
- MT5-Quant configured and working for local backtests
|
||||||
- Port 3000 open in macOS firewall (or whichever port MT5 uses — check below)
|
- Port 3000 open in macOS firewall (or whichever port MT5 uses)
|
||||||
|
|
||||||
**Linux server (agents)**
|
**Linux server (agents)**
|
||||||
- Wine 7.0+ (or Wine Staging for better compatibility)
|
- Wine 7.0+
|
||||||
- `metatester64.exe` from an MT5 installation
|
- `metatester64.exe` from an MT5 installation
|
||||||
- Access to the same MT5 data files (tick history) as the master — or let agents download on first run
|
- Access to the same MT5 data files (tick history) as the master
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1: Find the MT5 agent port on Mac
|
## Step 1: Find the MT5 agent port on Mac
|
||||||
|
|
||||||
@@ -31,7 +29,6 @@ ls ~/Library/Application\ Support/MetaQuotes/*/drive_c/Program\ Files/MetaTrader
|
|||||||
You'll see directories like:
|
You'll see directories like:
|
||||||
```
|
```
|
||||||
Agent-127.0.0.1-3000/ ← local agents (loopback)
|
Agent-127.0.0.1-3000/ ← local agents (loopback)
|
||||||
Agent-127.0.0.1-3001/
|
|
||||||
Agent-0.0.0.0-3000/ ← remote listener (if enabled)
|
Agent-0.0.0.0-3000/ ← remote listener (if enabled)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,69 +37,23 @@ If you don't see `Agent-0.0.0.0-*` directories, enable remote agents in MT5:
|
|||||||
|
|
||||||
Note the port number (default: 3000).
|
Note the port number (default: 3000).
|
||||||
|
|
||||||
---
|
## Step 2: Copy `metatester64.exe` to Linux
|
||||||
|
|
||||||
## Step 2: Open firewall on Mac
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check current firewall rules
|
|
||||||
sudo pfctl -s rules
|
|
||||||
|
|
||||||
# Allow incoming on agent port (example: 3000)
|
|
||||||
# Add to /etc/pf.conf or use macOS Firewall in System Settings
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use macOS System Settings → Privacy & Security → Firewall → Firewall Options → Add MetaTrader 5.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3: Install Wine on Linux server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo dpkg --add-architecture i386
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install wine64 wine32
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
wine64 --version
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4: Copy `metatester64.exe` to Linux
|
|
||||||
|
|
||||||
From your Mac MT5 installation:
|
From your Mac MT5 installation:
|
||||||
```bash
|
```bash
|
||||||
MAC_MT5="$HOME/Library/Application Support/MetaQuotes/Terminal/XXXXX/drive_c/Program Files/MetaTrader 5"
|
MAC_MT5="$HOME/Library/Application Support/MetaQuotes/Terminal/XXX/ drive_c/Program Files/MetaTrader 5"
|
||||||
|
|
||||||
scp "${MAC_MT5}/metatester64.exe" user@linux-server:~/mt5agents/
|
scp "${MAC_MT5}/metatester64.exe" user@linux-server:~/mt5agents/
|
||||||
scp "${MAC_MT5}/metaeditor64.exe" user@linux-server:~/mt5agents/ # not required for agents
|
|
||||||
```
|
|
||||||
|
|
||||||
Also copy the required DLLs (they're in the same directory):
|
|
||||||
```bash
|
|
||||||
scp "${MAC_MT5}"/*.dll user@linux-server:~/mt5agents/
|
scp "${MAC_MT5}"/*.dll user@linux-server:~/mt5agents/
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Step 3: Launch agents on Linux
|
||||||
|
|
||||||
## Step 5: Launch agents on Linux
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On the Linux server
|
|
||||||
cd ~/mt5agents/
|
cd ~/mt5agents/
|
||||||
|
|
||||||
# Replace MAC_IP with your Mac's local IP address
|
|
||||||
# Replace 8 with number of CPU cores - 1
|
|
||||||
wine64 metatester64.exe /server:192.168.1.100:3000 /agents:8
|
wine64 metatester64.exe /server:192.168.1.100:3000 /agents:8
|
||||||
```
|
```
|
||||||
|
|
||||||
**What this does:**
|
|
||||||
- Connects to your Mac's MT5 master at `192.168.1.100:3000`
|
|
||||||
- Registers 8 worker agents
|
|
||||||
- MT5 on Mac will show 8 new agents in the agent manager
|
|
||||||
|
|
||||||
**To run as a background service:**
|
**To run as a background service:**
|
||||||
```bash
|
```bash
|
||||||
nohup wine64 metatester64.exe /server:192.168.1.100:3000 /agents:8 \
|
nohup wine64 metatester64.exe /server:192.168.1.100:3000 /agents:8 \
|
||||||
@@ -110,9 +61,7 @@ nohup wine64 metatester64.exe /server:192.168.1.100:3000 /agents:8 \
|
|||||||
disown
|
disown
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Step 4: Verify agents appear in MT5
|
||||||
|
|
||||||
## Step 6: Verify agents appear in MT5
|
|
||||||
|
|
||||||
On your Mac, open MT5:
|
On your Mac, open MT5:
|
||||||
**View → Strategy Tester → Agents tab**
|
**View → Strategy Tester → Agents tab**
|
||||||
@@ -120,68 +69,38 @@ On your Mac, open MT5:
|
|||||||
You should see entries like:
|
You should see entries like:
|
||||||
```
|
```
|
||||||
Agent-192.168.1.200-3000 [Active]
|
Agent-192.168.1.200-3000 [Active]
|
||||||
Agent-192.168.1.200-3001 [Active]
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If agents appear as `[Inactive]`, check:
|
## Step 5: Configure MT5-Quant for remote agents
|
||||||
1. Mac firewall is allowing incoming connections on the agent port
|
|
||||||
2. Linux server can reach Mac IP: `ping 192.168.1.100`
|
|
||||||
3. Port is open: `nc -zv 192.168.1.100 3000`
|
|
||||||
|
|
||||||
---
|
In `config/mt5-quant.yaml`:
|
||||||
|
|
||||||
## Step 7: Configure MT5-Quant for remote agents
|
|
||||||
|
|
||||||
In `config/MT5-Quant.yaml`:
|
|
||||||
```yaml
|
```yaml
|
||||||
optimization:
|
optimization:
|
||||||
remote_agents:
|
remote_agents:
|
||||||
enabled: true
|
enabled: true
|
||||||
check_agent_count: true # Verify remote agents are connected before launching
|
check_agent_count: true
|
||||||
min_agents: 4 # Require at least N agents before optimizing
|
min_agents: 4
|
||||||
```
|
```
|
||||||
|
|
||||||
MT5-Quant will log the agent count before launching optimization:
|
|
||||||
```
|
|
||||||
[optimize] Local agents: 9, Remote agents: 8, Total: 17
|
|
||||||
[optimize] Estimated completion: ~105 minutes (17,640 passes at 17 agents)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tick Data Sync
|
## Tick Data Sync
|
||||||
|
|
||||||
Remote agents need tick history to replay trades. On first run, MT5 automatically downloads ticks from the broker for each symbol+period combination tested. This can take 10-30 minutes per symbol.
|
On first run, MT5 automatically downloads ticks from the broker. Pre-populate on Linux:
|
||||||
|
|
||||||
**Speed up first run:** Pre-populate the tick cache on the Linux server by copying from Mac:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On Mac — find tick data location
|
# On Mac — find tick data location
|
||||||
find ~/Library/Application\ Support/MetaQuotes -name "*.bin" | grep "XAUUSD"
|
find ~/Library/Application\ Support/MetaQuotes -name "*.bin" | grep "XAUUSD"
|
||||||
|
|
||||||
# Typical path:
|
# Copy to Linux
|
||||||
# Terminal/XXXXX/drive_c/users/user/AppData/Roaming/MetaQuotes/Terminal/Common/Files/
|
|
||||||
|
|
||||||
scp -r "${TICK_DIR}" user@linux-server:~/mt5agents/ticks/
|
scp -r "${TICK_DIR}" user@linux-server:~/mt5agents/ticks/
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Agents connect then immediately disconnect**
|
**Agents connect then immediately disconnect**
|
||||||
- MT5 version mismatch between `metatester64.exe` (from Mac) and the master. Use the exact same build number.
|
- MT5 version mismatch between `metatester64.exe` (from Mac) and the master. Use the exact same build number.
|
||||||
- Check `~/mt5agents/agents.log` for Wine errors.
|
|
||||||
|
|
||||||
**Agents show as connected but don't receive work**
|
**Agents show as connected but don't receive work**
|
||||||
- MT5 sometimes requires optimization to be started before assigning work to new agents.
|
|
||||||
- Start an optimization from the MT5 GUI first to "activate" remote agents, then cancel it and use MT5-Quant.
|
- Start an optimization from the MT5 GUI first to "activate" remote agents, then cancel it and use MT5-Quant.
|
||||||
|
|
||||||
**Performance is slower with remote agents**
|
**Performance is slower with remote agents**
|
||||||
- Network latency between Mac and Linux server. Results are sent over TCP after each pass.
|
- Use wired connection. WiFi or WAN: significant overhead.
|
||||||
- Local gigabit network: negligible. WiFi or WAN: significant overhead. Use wired connection.
|
|
||||||
|
|
||||||
**Linux server runs out of memory**
|
|
||||||
- Each `metatester64.exe` instance uses ~200-400MB.
|
|
||||||
- 8 agents = ~2-3GB RAM. Size agent count accordingly.
|
|
||||||
|
|||||||
@@ -17,12 +17,17 @@ If using CrossOver, confirm bottle is named `MetaTrader5`.
|
|||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
|
Install Wine 7.0+ and confirm on PATH:
|
||||||
```bash
|
```bash
|
||||||
sudo apt install wine64 # Debian/Ubuntu
|
# Debian/Ubuntu
|
||||||
sudo dnf install wine # Fedora/RHEL
|
sudo apt install wine64
|
||||||
which wine64 # confirm on PATH
|
# Fedora/RHEL
|
||||||
|
sudo dnf install wine
|
||||||
|
which wine64
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ask your LLM platform to install Wine if it's missing.
|
||||||
|
|
||||||
## terminal64.exe Missing
|
## terminal64.exe Missing
|
||||||
|
|
||||||
MT5 unpacks `terminal64.exe` only after first launch.
|
MT5 unpacks `terminal64.exe` only after first launch.
|
||||||
|
|||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
# VS Code MCP Integration Setup
|
|
||||||
|
|
||||||
## Quick Setup
|
|
||||||
|
|
||||||
### Option 1: Download Prebuilt Binary (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# macOS (Apple Silicon)
|
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-macos-arm64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
|
|
||||||
# Linux (x64)
|
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-linux-x64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Build from Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configure VS Code
|
|
||||||
|
|
||||||
### Method 1: Command Palette (Recommended)
|
|
||||||
|
|
||||||
1. Press `Cmd/Ctrl + Shift + P`
|
|
||||||
2. Run `MCP: Add Server`
|
|
||||||
3. Choose **Workspace** or **User** scope
|
|
||||||
4. Enter server name: `mt5-quant`
|
|
||||||
5. Enter command: `/path/to/mt5-quant/mcp-server/bin/mt5-quant`
|
|
||||||
|
|
||||||
### Method 2: Edit mcp.json Directly
|
|
||||||
|
|
||||||
Add to `.vscode/mcp.json` in your workspace:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"servers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p .vscode
|
|
||||||
cat > .vscode/mcp.json << 'EOF'
|
|
||||||
{
|
|
||||||
"servers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 3: VS Code CLI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
code --add-mcp '{"name":"mt5-quant","command":"/path/to/mt5-quant/mcp-server/bin/mt5-quant"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verify Setup
|
|
||||||
|
|
||||||
In Copilot chat, type:
|
|
||||||
|
|
||||||
```
|
|
||||||
Run verify_setup
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output:
|
|
||||||
```
|
|
||||||
Wine: /Applications/MetaTrader 5.app/.../wine64
|
|
||||||
MT5 dir: ~/Library/Application Support/.../MetaTrader 5
|
|
||||||
Display: gui
|
|
||||||
Arch: arch -x86_64
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Locations
|
|
||||||
|
|
||||||
| Scope | Path | Use Case |
|
|
||||||
|-------|------|----------|
|
|
||||||
| Workspace | `.vscode/mcp.json` | Share with team via source control |
|
|
||||||
| User | `~/.vscode/mcp.json` | Personal tools across all projects |
|
|
||||||
| Dev Container | `devcontainer.json` → `customizations.vscode.mcp` | Containerized environments |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### MCP server not appearing
|
|
||||||
|
|
||||||
1. Open **Output** panel (`Cmd/Ctrl + Shift + U`)
|
|
||||||
2. Select **MCP** from dropdown
|
|
||||||
3. Check for connection errors
|
|
||||||
4. Verify the path is absolute
|
|
||||||
|
|
||||||
### Config not found
|
|
||||||
|
|
||||||
The binary auto-detects its config, but you can also:
|
|
||||||
1. Run `setup.sh` to create `config/mt5-quant.yaml`
|
|
||||||
2. Or let the binary auto-discover on first run
|
|
||||||
|
|
||||||
### Dev Container Setup
|
|
||||||
|
|
||||||
Add to `.devcontainer/devcontainer.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"mcp": {
|
|
||||||
"servers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Differences from Other IDEs
|
|
||||||
|
|
||||||
VS Code uses `servers` (not `mcpServers`) in the JSON structure:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"servers": { // ← VS Code uses "servers"
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Other platforms use `mcpServers`.
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [VS Code MCP Documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers)
|
|
||||||
- [MCP Configuration Reference](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration)
|
|
||||||
- [MT5-Quant Tools Reference](./MCP_TOOLS.md)
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
# Windsurf MCP Integration Setup
|
|
||||||
|
|
||||||
## Quick Setup
|
|
||||||
|
|
||||||
### Option 1: Download Prebuilt Binary (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# macOS (Apple Silicon)
|
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-macos-arm64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
|
|
||||||
# Linux (x64)
|
|
||||||
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mcp-mt5-quant-linux-x64.tar.gz
|
|
||||||
tar -xzf mt5.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Build from Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/build-rust.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configure Windsurf
|
|
||||||
|
|
||||||
Edit `~/.codeium/windsurf/mcp_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/path/to/mt5-quant/mcp-server/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use the automated setup:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install binary to standard location
|
|
||||||
cp mcp-server/bin/mt5-quant ~/.local/bin/
|
|
||||||
|
|
||||||
# Then configure
|
|
||||||
bash scripts/setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Restart Windsurf
|
|
||||||
Close and reopen Windsurf to load the MCP server.
|
|
||||||
|
|
||||||
### 4. Verify
|
|
||||||
In Windsurf chat, test with:
|
|
||||||
```
|
|
||||||
Run verify_setup
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment to Multiple Machines
|
|
||||||
|
|
||||||
### Build for Distribution
|
|
||||||
```bash
|
|
||||||
# Build release binary
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
# Create tarball
|
|
||||||
tar -czf mt5-quant-macos-arm64.tar.gz -C target/release mt5-quant
|
|
||||||
|
|
||||||
# Deploy to remote server
|
|
||||||
scp mt5-quant-macos-arm64.tar.gz user@server:~/
|
|
||||||
ssh user@server "tar -xzf mt5-quant-macos-arm64.tar.gz -C /opt/"
|
|
||||||
ssh user@server "ln -s /opt/mt5-quant /usr/local/bin/"
|
|
||||||
|
|
||||||
# Copy config
|
|
||||||
scp -r config/mt5-quant.yaml user@server:~/.config/mt5-quant/config/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Target Machine Requirements
|
|
||||||
- MetaTrader 5 installed (via Wine/CrossOver)
|
|
||||||
- Config file at `~/.config/mt5-quant/config/mt5-quant.yaml`
|
|
||||||
- **NO Python required!**
|
|
||||||
|
|
||||||
### Windsurf Config on Target Machine
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "/usr/local/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The binary auto-detects its config location. No environment variables needed.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### MCP server not appearing
|
|
||||||
1. Check Windsurf logs: `~/.windsurf/logs/`
|
|
||||||
2. Verify executable path is absolute
|
|
||||||
3. Test executable manually: `./mcp-server/bin/mt5-quant --help`
|
|
||||||
|
|
||||||
### Config not found
|
|
||||||
Set `MT5_MCP_HOME` environment variable or ensure config is at default location:
|
|
||||||
- macOS: `~/.config/mt5-quant/config/mt5-quant.yaml`
|
|
||||||
|
|
||||||
### Permission denied
|
|
||||||
```bash
|
|
||||||
chmod +x /path/to/mt5-quant
|
|
||||||
```
|
|
||||||
+4
-4
@@ -7,13 +7,13 @@
|
|||||||
"url": "https://github.com/masdevid/mt5-quant",
|
"url": "https://github.com/masdevid/mt5-quant",
|
||||||
"source": "github"
|
"source": "github"
|
||||||
},
|
},
|
||||||
"version": "1.32.3",
|
"version": "1.33.0",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"registryType": "mcpb",
|
"registryType": "mcpb",
|
||||||
"version": "1.32.3",
|
"version": "1.33.0",
|
||||||
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.32.3/mcp-mt5-quant-macos-arm64.tar.gz",
|
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.33.0/mcp-mt5-quant-macos-arm64.tar.gz",
|
||||||
"fileSha256": "4c0b396f48c64334455e344644eaf6eaa6783a80fbf148688193e0108eadb949",
|
"fileSha256": "54c6fd9d1f0d009bad1fa001582607377442ecfca908e3692b09d4c6b2732074",
|
||||||
"transport": {
|
"transport": {
|
||||||
"type": "stdio"
|
"type": "stdio"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -247,19 +247,21 @@ impl Config {
|
|||||||
|
|
||||||
fn find_wine(home: &Path) -> Option<String> {
|
fn find_wine(home: &Path) -> Option<String> {
|
||||||
let candidates: &[PathBuf] = &[
|
let candidates: &[PathBuf] = &[
|
||||||
// macOS: bundled with the official MT5 app
|
// macOS: bundled with the official MT5 app (binary is just named 'wine' on recent builds)
|
||||||
|
PathBuf::from("/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine"),
|
||||||
PathBuf::from("/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine64"),
|
PathBuf::from("/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine64"),
|
||||||
// macOS: CrossOver
|
// macOS: CrossOver (new versions may use 'wine', older ones 'wine64')
|
||||||
|
home.join("Applications/CrossOver.app/Contents/SharedSupport/CrossOver/wine/bin/wine"),
|
||||||
home.join("Applications/CrossOver.app/Contents/SharedSupport/CrossOver/wine/bin/wine64"),
|
home.join("Applications/CrossOver.app/Contents/SharedSupport/CrossOver/wine/bin/wine64"),
|
||||||
// macOS: Homebrew (Apple Silicon)
|
// macOS: Homebrew Apple Silicon (prefer 'wine', fall back to 'wine64')
|
||||||
PathBuf::from("/opt/homebrew/bin/wine64"),
|
|
||||||
PathBuf::from("/opt/homebrew/bin/wine"),
|
PathBuf::from("/opt/homebrew/bin/wine"),
|
||||||
// macOS: Homebrew (Intel)
|
PathBuf::from("/opt/homebrew/bin/wine64"),
|
||||||
PathBuf::from("/usr/local/bin/wine64"),
|
// macOS: Homebrew Intel
|
||||||
PathBuf::from("/usr/local/bin/wine"),
|
PathBuf::from("/usr/local/bin/wine"),
|
||||||
|
PathBuf::from("/usr/local/bin/wine64"),
|
||||||
// Linux
|
// Linux
|
||||||
PathBuf::from("/usr/bin/wine64"),
|
|
||||||
PathBuf::from("/usr/bin/wine"),
|
PathBuf::from("/usr/bin/wine"),
|
||||||
|
PathBuf::from("/usr/bin/wine64"),
|
||||||
];
|
];
|
||||||
candidates.iter()
|
candidates.iter()
|
||||||
.find(|p| p.exists())
|
.find(|p| p.exists())
|
||||||
|
|||||||
+257
-75
@@ -6,6 +6,27 @@ use std::process::{Command, Stdio};
|
|||||||
|
|
||||||
use crate::models::Config;
|
use crate::models::Config;
|
||||||
|
|
||||||
|
/// Read a file that may be UTF-16LE (with BOM) or UTF-8, returning a UTF-8 String.
|
||||||
|
/// MT5 .set and .ini files are typically UTF-16LE with BOM (0xFF 0xFE).
|
||||||
|
fn read_file_as_utf8(path: &Path) -> Result<String> {
|
||||||
|
let bytes = fs::read(path)?;
|
||||||
|
|
||||||
|
// Check for UTF-16LE BOM (0xFF 0xFE)
|
||||||
|
if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE {
|
||||||
|
// UTF-16LE with BOM - skip the 2-byte BOM and decode
|
||||||
|
let utf16_data: Vec<u16> = bytes[2..]
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16(&utf16_data)
|
||||||
|
.map_err(|e| anyhow!("Failed to decode UTF-16LE: {}", e))
|
||||||
|
} else {
|
||||||
|
// Try UTF-8
|
||||||
|
String::from_utf8(bytes)
|
||||||
|
.map_err(|e| anyhow!("Failed to decode as UTF-8: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct OptimizationParams {
|
pub struct OptimizationParams {
|
||||||
pub expert: String,
|
pub expert: String,
|
||||||
pub set_file: String,
|
pub set_file: String,
|
||||||
@@ -13,7 +34,6 @@ pub struct OptimizationParams {
|
|||||||
pub from_date: String,
|
pub from_date: String,
|
||||||
pub to_date: String,
|
pub to_date: String,
|
||||||
pub deposit: u32,
|
pub deposit: u32,
|
||||||
pub model: u8,
|
|
||||||
pub leverage: u32,
|
pub leverage: u32,
|
||||||
pub currency: String,
|
pub currency: String,
|
||||||
}
|
}
|
||||||
@@ -27,7 +47,6 @@ impl Default for OptimizationParams {
|
|||||||
from_date: String::new(),
|
from_date: String::new(),
|
||||||
to_date: String::new(),
|
to_date: String::new(),
|
||||||
deposit: 10000,
|
deposit: 10000,
|
||||||
model: 0,
|
|
||||||
leverage: 500,
|
leverage: 500,
|
||||||
currency: "USD".to_string(),
|
currency: "USD".to_string(),
|
||||||
}
|
}
|
||||||
@@ -78,7 +97,8 @@ impl OptimizationRunner {
|
|||||||
let log_file = PathBuf::from(format!("/tmp/mt5opt_{}.log", timestamp));
|
let log_file = PathBuf::from(format!("/tmp/mt5opt_{}.log", timestamp));
|
||||||
|
|
||||||
// Count combinations
|
// Count combinations
|
||||||
let combinations = self.count_combinations(¶ms.set_file)?;
|
let combinations = self.count_combinations(¶ms.set_file)
|
||||||
|
.map_err(|e| anyhow!("count_combinations failed: {}", e))?;
|
||||||
|
|
||||||
// Get paths
|
// Get paths
|
||||||
let mt5_dir = self.config.terminal_dir.as_ref()
|
let mt5_dir = self.config.terminal_dir.as_ref()
|
||||||
@@ -89,50 +109,167 @@ impl OptimizationRunner {
|
|||||||
// Write .set file as UTF-16LE with BOM directly to MT5 tester directory
|
// Write .set file as UTF-16LE with BOM directly to MT5 tester directory
|
||||||
let wine_prefix_dir = self.get_wine_prefix_dir(mt5_dir)?;
|
let wine_prefix_dir = self.get_wine_prefix_dir(mt5_dir)?;
|
||||||
let tester_dir = wine_prefix_dir.join("drive_c/Program Files/MetaTrader 5/MQL5/Profiles/Tester");
|
let tester_dir = wine_prefix_dir.join("drive_c/Program Files/MetaTrader 5/MQL5/Profiles/Tester");
|
||||||
fs::create_dir_all(&tester_dir)?;
|
fs::create_dir_all(&tester_dir).map_err(|e| anyhow!("create_dir_all({}) failed: {}", tester_dir.display(), e))?;
|
||||||
let dst_set_file = tester_dir.join(format!("{}.set", params.expert));
|
let dst_set_file = tester_dir.join(format!("{}.set", params.expert));
|
||||||
self.write_utf16le_set(¶ms.set_file, &dst_set_file)?;
|
self.write_utf16le_set(¶ms.set_file, &dst_set_file)
|
||||||
|
.map_err(|e| anyhow!("write_utf16le_set({}) failed: {}", dst_set_file.display(), e))?;
|
||||||
|
|
||||||
// Reset OptMode in terminal.ini
|
// Reset OptMode in terminal.ini
|
||||||
self.reset_optmode(mt5_dir)?;
|
// Patch terminal.ini [Tester] section with optimization params (primary mechanism)
|
||||||
|
let terminal_ini = if Path::new(mt5_dir).join("config").exists() {
|
||||||
|
Path::new(mt5_dir).join("config").join("terminal.ini")
|
||||||
|
} else {
|
||||||
|
Path::new(mt5_dir).join("terminal.ini")
|
||||||
|
};
|
||||||
|
let mt5_ini_text = if terminal_ini.exists() {
|
||||||
|
read_file_as_utf8(&terminal_ini).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let expert_path = if let Some(experts_dir) = &self.config.experts_dir {
|
||||||
|
let nested = Path::new(experts_dir).join(¶ms.expert).join(format!("{}.mq5", params.expert));
|
||||||
|
if nested.exists() {
|
||||||
|
format!("Experts\\{}\\{}.ex5", params.expert, params.expert)
|
||||||
|
} else {
|
||||||
|
format!("Experts\\{}.ex5", params.expert)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("Experts\\{}.ex5", params.expert)
|
||||||
|
};
|
||||||
|
let tester_section = format!(
|
||||||
|
"[Tester]\n\
|
||||||
|
Expert={}\n\
|
||||||
|
ExpertParameters={}.set\n\
|
||||||
|
Symbol={}\n\
|
||||||
|
Period=M1\n\
|
||||||
|
Model=4\n\
|
||||||
|
FromDate={}\n\
|
||||||
|
ToDate={}\n\
|
||||||
|
ForwardMode=0\n\
|
||||||
|
Deposit={}\n\
|
||||||
|
Currency={}\n\
|
||||||
|
ProfitInPips=0\n\
|
||||||
|
Leverage={}\n\
|
||||||
|
Execution=10\n\
|
||||||
|
Optimization=2\n\
|
||||||
|
Agents=10\n\
|
||||||
|
Visual=0\n\
|
||||||
|
Report=reports\\opt_report.htm\n\
|
||||||
|
ReplaceReport=1\n\
|
||||||
|
ShutdownTerminal=1",
|
||||||
|
expert_path, params.expert, params.symbol,
|
||||||
|
params.from_date, params.to_date, params.deposit, params.currency, params.leverage,
|
||||||
|
);
|
||||||
|
let agents_section = "\
|
||||||
|
[Agents]\n\
|
||||||
|
Agent0000=11111111-1111-1111-1111-111111111111\n\
|
||||||
|
AgentStatus0000=3\n\
|
||||||
|
AgentState0000=0\n\
|
||||||
|
Enabled0000=1\n\
|
||||||
|
IP0000=127.0.0.1\n\
|
||||||
|
Port0000=3000\n\
|
||||||
|
Agent0001=22222222-2222-2222-2222-222222222222\n\
|
||||||
|
AgentStatus0001=3\n\
|
||||||
|
AgentState0001=0\n\
|
||||||
|
Enabled0001=1\n\
|
||||||
|
IP0001=127.0.0.1\n\
|
||||||
|
Port0001=3001\n\
|
||||||
|
Agent0002=33333333-3333-3333-3333-333333333333\n\
|
||||||
|
AgentStatus0002=3\n\
|
||||||
|
AgentState0002=0\n\
|
||||||
|
Enabled0002=1\n\
|
||||||
|
IP0002=127.0.0.1\n\
|
||||||
|
Port0002=3002\n\
|
||||||
|
Agent0003=44444444-4444-4444-4444-444444444444\n\
|
||||||
|
AgentStatus0003=3\n\
|
||||||
|
AgentState0003=0\n\
|
||||||
|
Enabled0003=1\n\
|
||||||
|
IP0003=127.0.0.1\n\
|
||||||
|
Port0003=3003";
|
||||||
|
let updated_ini = Self::patch_ini_section(&mt5_ini_text, "Tester", &tester_section);
|
||||||
|
// Strip any stale [Agents] sections from previous runs, then append fresh one
|
||||||
|
let cleaned = Self::strip_ini_section(&updated_ini, "Agents");
|
||||||
|
let final_ini = format!("{}\n{}", cleaned.trim_end(), agents_section);
|
||||||
|
let mut utf16_out: Vec<u8> = vec![0xFF, 0xFE];
|
||||||
|
utf16_out.extend(final_ini.encode_utf16().flat_map(|c| c.to_le_bytes()));
|
||||||
|
fs::write(&terminal_ini, utf16_out)?;
|
||||||
|
|
||||||
// Get Wine prefix directory
|
// Write /config: INI to trigger tester/optimizer mode
|
||||||
let wine_prefix_dir = self.get_wine_prefix_dir(mt5_dir)?;
|
// For /config: format, Expert path is relative to MQL5/Experts/ (no Experts\ prefix)
|
||||||
|
let opt_config_win = r"C:\mt5opt_config.ini";
|
||||||
|
let opt_config_host = wine_prefix_dir.join("drive_c").join("mt5opt_config.ini");
|
||||||
|
let mut opt_ini = String::new();
|
||||||
|
if let Some(login) = &self.config.backtest_login {
|
||||||
|
if let Some(server) = &self.config.backtest_server {
|
||||||
|
opt_ini.push_str("[Common]\n");
|
||||||
|
opt_ini.push_str(&format!("Login={}\n", login));
|
||||||
|
opt_ini.push_str(&format!("Server={}\n", server));
|
||||||
|
if let Some(password) = &self.config.backtest_password {
|
||||||
|
opt_ini.push_str(&format!("Password={}\n", password));
|
||||||
|
}
|
||||||
|
opt_ini.push_str("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opt_ini.push_str("[Tester]\n");
|
||||||
|
opt_ini.push_str(&format!("Expert={}.ex5\n", params.expert));
|
||||||
|
opt_ini.push_str(&format!("ExpertParameters={}.set\n", params.expert));
|
||||||
|
opt_ini.push_str(&format!("Symbol={}\n", params.symbol));
|
||||||
|
opt_ini.push_str("Period=M1\n");
|
||||||
|
opt_ini.push_str("Optimization=2\n");
|
||||||
|
opt_ini.push_str(&format!("FromDate={}\n", params.from_date));
|
||||||
|
opt_ini.push_str(&format!("ToDate={}\n", params.to_date));
|
||||||
|
opt_ini.push_str("ForwardMode=0\n");
|
||||||
|
opt_ini.push_str(&format!("Deposit={}\n", params.deposit));
|
||||||
|
opt_ini.push_str(&format!("Currency={}\n", params.currency));
|
||||||
|
opt_ini.push_str("ProfitInPips=0\n");
|
||||||
|
opt_ini.push_str(&format!("Leverage={}\n", params.leverage));
|
||||||
|
opt_ini.push_str("Execution=10\n");
|
||||||
|
opt_ini.push_str("Visual=0\n");
|
||||||
|
opt_ini.push_str("Report=reports\\opt_report.htm\n");
|
||||||
|
opt_ini.push_str("ReplaceReport=1\n");
|
||||||
|
opt_ini.push_str("ShutdownTerminal=1\n");
|
||||||
|
fs::write(&opt_config_host, opt_ini.as_bytes())?;
|
||||||
|
|
||||||
// Build optimization INI
|
// Build launch script (macOS-compatible with /config: to trigger tester mode)
|
||||||
let ini_path = wine_prefix_dir.join("drive_c/mt5mcp_backtest.ini");
|
let wine_bin = Path::new(wine_exe);
|
||||||
let ini_content = format!(r#"[Tester]
|
let wine_root = wine_bin
|
||||||
Expert={}
|
.parent()
|
||||||
Symbol={}
|
.and_then(|p| p.parent())
|
||||||
Period=M5
|
.ok_or_else(|| anyhow!("Cannot derive Wine root from wine_exe"))?;
|
||||||
Deposit={}
|
let ext_libs = wine_root.join("lib").join("external");
|
||||||
Currency={}
|
let wine_libs = wine_root.join("lib");
|
||||||
Leverage={}
|
let dyld = format!("{}:{}:/usr/lib:/usr/local/lib",
|
||||||
Model={}
|
ext_libs.display(), wine_libs.display());
|
||||||
FromDate={}
|
let terminal_host = wine_prefix_dir.join("drive_c")
|
||||||
ToDate={}
|
.join("Program Files").join("MetaTrader 5").join("terminal64.exe");
|
||||||
Report=C:\mt5mcp_opt_report
|
|
||||||
Optimization=2
|
|
||||||
ExpertParameters={}.set
|
|
||||||
ShutdownTerminal=1
|
|
||||||
"#, params.expert, params.symbol, params.deposit, params.currency,
|
|
||||||
params.leverage, params.model, params.from_date, params.to_date, params.expert);
|
|
||||||
fs::write(&ini_path, ini_content)?;
|
|
||||||
|
|
||||||
// Build batch file
|
let script = format!(
|
||||||
let batch_path = wine_prefix_dir.join("drive_c/mt5mcp_run.bat");
|
"#!/bin/sh\n\
|
||||||
let batch_content = format!(r#"@echo off
|
export DYLD_FALLBACK_LIBRARY_PATH='{dyld}'\n\
|
||||||
"C:\Program Files\MetaTrader 5\terminal64.exe" /config:C:\mt5mcp_backtest.ini
|
export WINEPREFIX='{prefix}'\n\
|
||||||
"#);
|
export WINEDEBUG='-all'\n\
|
||||||
fs::write(&batch_path, batch_content)?;
|
nohup '{wine}' '{terminal}' '/config:{config}' >/dev/null 2>&1 &\n",
|
||||||
|
dyld = dyld,
|
||||||
|
prefix = wine_prefix_dir.display(),
|
||||||
|
wine = wine_exe,
|
||||||
|
terminal = terminal_host.display(),
|
||||||
|
config = opt_config_win,
|
||||||
|
);
|
||||||
|
|
||||||
// Launch detached process
|
let script_path = std::env::temp_dir().join("mt5opt_launch.sh");
|
||||||
let cmd = format!("cmd.exe /c 'C:\\mt5mcp_run.bat'");
|
fs::write(&script_path, &script)?;
|
||||||
let child = Command::new(wine_exe)
|
#[cfg(unix)]
|
||||||
.arg(&cmd)
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = Command::new("/bin/sh")
|
||||||
|
.arg(&script_path)
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.spawn()?;
|
.spawn()
|
||||||
|
.map_err(|e| anyhow!("spawn /bin/sh {} failed: {}", script_path.display(), e))?;
|
||||||
|
|
||||||
let pid = child.id();
|
let pid = child.id();
|
||||||
|
|
||||||
@@ -150,7 +287,7 @@ ShutdownTerminal=1
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn count_combinations(&self, set_file: &str) -> Result<u64> {
|
fn count_combinations(&self, set_file: &str) -> Result<u64> {
|
||||||
let content = fs::read_to_string(set_file)?;
|
let content = read_file_as_utf8(Path::new(set_file))?;
|
||||||
let mut total: u64 = 1;
|
let mut total: u64 = 1;
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
@@ -179,13 +316,23 @@ ShutdownTerminal=1
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_utf16le_set(&self, src: &str, dst: &Path) -> Result<()> {
|
fn write_utf16le_set(&self, src: &str, dst: &Path) -> Result<()> {
|
||||||
let content = fs::read_to_string(src)?;
|
let content = read_file_as_utf8(Path::new(src))?;
|
||||||
|
|
||||||
// Create parent directory if needed
|
// Create parent directory if needed
|
||||||
if let Some(parent) = dst.parent() {
|
if let Some(parent) = dst.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove existing file if read-only from previous run
|
||||||
|
if dst.exists() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let _ = fs::set_permissions(dst, fs::Permissions::from_mode(0o644));
|
||||||
|
}
|
||||||
|
let _ = fs::remove_file(dst);
|
||||||
|
}
|
||||||
|
|
||||||
// Write UTF-16LE with BOM
|
// Write UTF-16LE with BOM
|
||||||
let mut utf16_content: Vec<u16> = vec![0xFEFF]; // BOM
|
let mut utf16_content: Vec<u16> = vec![0xFEFF]; // BOM
|
||||||
utf16_content.extend(content.encode_utf16());
|
utf16_content.extend(content.encode_utf16());
|
||||||
@@ -195,50 +342,18 @@ ShutdownTerminal=1
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
fs::write(dst, bytes)?;
|
fs::write(dst, bytes)?;
|
||||||
|
|
||||||
// Make read-only
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
fs::set_permissions(dst, fs::Permissions::from_mode(0o444))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_optmode(&self, mt5_dir: &str) -> Result<()> {
|
|
||||||
let terminal_ini = Path::new(mt5_dir).join("terminal.ini");
|
|
||||||
|
|
||||||
if !terminal_ini.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = fs::read_to_string(&terminal_ini)?;
|
|
||||||
let updated = content
|
|
||||||
.lines()
|
|
||||||
.map(|line| {
|
|
||||||
if line.starts_with("OptMode=") {
|
|
||||||
"OptMode=0".to_string()
|
|
||||||
} else if line.starts_with("LastOptimization=") {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
line.to_string()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(|l| !l.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
fs::write(&terminal_ini, updated)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_wine_prefix_dir(&self, mt5_dir: &str) -> Result<PathBuf> {
|
fn get_wine_prefix_dir(&self, mt5_dir: &str) -> Result<PathBuf> {
|
||||||
let path = Path::new(mt5_dir);
|
let path = Path::new(mt5_dir);
|
||||||
// Go up two levels: .../drive_c/Program Files/MetaTrader 5 -> .../drive_c
|
// Go up three levels: .../drive_c/Program Files/MetaTrader 5 -> .../net.metaquotes.wine.metatrader5
|
||||||
|
// (same as backtest pipeline)
|
||||||
let prefix_dir = path
|
let prefix_dir = path
|
||||||
.parent()
|
.parent()
|
||||||
.and_then(|p| p.parent())
|
.and_then(|p| p.parent())
|
||||||
|
.and_then(|p| p.parent())
|
||||||
.ok_or_else(|| anyhow!("Cannot determine Wine prefix from terminal_dir"))?;
|
.ok_or_else(|| anyhow!("Cannot determine Wine prefix from terminal_dir"))?;
|
||||||
Ok(prefix_dir.to_path_buf())
|
Ok(prefix_dir.to_path_buf())
|
||||||
}
|
}
|
||||||
@@ -276,6 +391,73 @@ ShutdownTerminal=1
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace a [section] in an INI string — removes old content and inserts new.
|
||||||
|
fn patch_ini_section(text: &str, section: &str, new_content: &str) -> String {
|
||||||
|
let section_header = format!("[{}]", section);
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut in_section = false;
|
||||||
|
let mut section_found = false;
|
||||||
|
|
||||||
|
for line in text.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed == section_header {
|
||||||
|
in_section = true;
|
||||||
|
section_found = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if in_section {
|
||||||
|
if trimmed.starts_with('[') {
|
||||||
|
in_section = false;
|
||||||
|
result.push_str(new_content);
|
||||||
|
if !new_content.ends_with('\n') {
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
result.push_str(line);
|
||||||
|
result.push('\n');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push_str(line);
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if !section_found {
|
||||||
|
if !result.is_empty() && !result.ends_with('\n') {
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
result.push_str(new_content);
|
||||||
|
result.push('\n');
|
||||||
|
} else if in_section {
|
||||||
|
result.push_str(new_content);
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove all lines belonging to a [section] from the INI text.
|
||||||
|
fn strip_ini_section(text: &str, section: &str) -> String {
|
||||||
|
let header = format!("[{}]", section);
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut skipping = false;
|
||||||
|
for line in text.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed == header {
|
||||||
|
skipping = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if skipping && trimmed.starts_with('[') {
|
||||||
|
skipping = false;
|
||||||
|
}
|
||||||
|
if !skipping {
|
||||||
|
result.push_str(line);
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_job_status(&self, job_id: &str) -> Result<serde_json::Value> {
|
pub fn get_job_status(&self, job_id: &str) -> Result<serde_json::Value> {
|
||||||
let jobs_dir = Path::new(".mt5mcp_jobs");
|
let jobs_dir = Path::new(".mt5mcp_jobs");
|
||||||
let meta_path = jobs_dir.join(format!("{}.json", job_id));
|
let meta_path = jobs_dir.join(format!("{}.json", job_id));
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
pub fn tool_check_update() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "check_update",
|
||||||
|
"description": "Check if a newer version of mt5-quant is available on GitHub. A background check runs automatically on the first tool call of each session; this tool returns that cached result instantly or fetches it on demand.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tool_update() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "update",
|
||||||
|
"description": "Download and install the latest mt5-quant binary from GitHub Releases, then replace the current executable in place. Restart the MCP connection after updating to load the new version.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ pub async fn handle_run_optimization(config: &Config, args: &Value) -> Result<Va
|
|||||||
from_date: from_date.to_string(),
|
from_date: from_date.to_string(),
|
||||||
to_date: to_date.to_string(),
|
to_date: to_date.to_string(),
|
||||||
deposit: args.get("deposit").and_then(|v| v.as_u64()).unwrap_or(10000) as u32,
|
deposit: args.get("deposit").and_then(|v| v.as_u64()).unwrap_or(10000) as u32,
|
||||||
model: 0,
|
|
||||||
leverage: args.get("leverage").and_then(|v| v.as_u64()).unwrap_or(500) as u32,
|
leverage: args.get("leverage").and_then(|v| v.as_u64()).unwrap_or(500) as u32,
|
||||||
currency: args.get("currency").and_then(|v| v.as_str()).unwrap_or("USD").to_string(),
|
currency: args.get("currency").and_then(|v| v.as_str()).unwrap_or("USD").to_string(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,16 +3,32 @@ use serde_json::{json, Value};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use crate::models::Config;
|
use crate::models::Config;
|
||||||
|
|
||||||
|
/// Read a file that may be UTF-16LE (with BOM) or UTF-8, returning a UTF-8 String.
|
||||||
|
fn read_file_as_utf8(path: &str) -> Result<String> {
|
||||||
|
let bytes = fs::read(path)?;
|
||||||
|
if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE {
|
||||||
|
let utf16_data: Vec<u16> = bytes[2..]
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16(&utf16_data)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to decode UTF-16LE: {}", e))
|
||||||
|
} else {
|
||||||
|
String::from_utf8(bytes)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to decode as UTF-8: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_read_set_file(args: &Value) -> Result<Value> {
|
pub async fn handle_read_set_file(args: &Value) -> Result<Value> {
|
||||||
let path = args.get("path")
|
let path = args.get("path")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or_else(|| anyhow::anyhow!("path is required"))?;
|
.ok_or_else(|| anyhow::anyhow!("path is required"))?;
|
||||||
|
|
||||||
let content = fs::read_to_string(path)?;
|
let content = read_file_as_utf8(path)?;
|
||||||
let mut params = serde_json::Map::new();
|
let mut params = serde_json::Map::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
if let Some((key, value)) = line.split_once(':') {
|
if let Some((key, value)) = line.split_once('=') {
|
||||||
let key = key.trim();
|
let key = key.trim();
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
|
|
||||||
@@ -86,7 +102,7 @@ pub async fn handle_patch_set_file(args: &Value) -> Result<Value> {
|
|||||||
.and_then(|v| v.as_object())
|
.and_then(|v| v.as_object())
|
||||||
.ok_or_else(|| anyhow::anyhow!("patches object is required"))?;
|
.ok_or_else(|| anyhow::anyhow!("patches object is required"))?;
|
||||||
|
|
||||||
let content = fs::read_to_string(path)?;
|
let content = read_file_as_utf8(path)?;
|
||||||
let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
|
let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
|
||||||
let mut patched_count = 0;
|
let mut patched_count = 0;
|
||||||
|
|
||||||
@@ -164,8 +180,8 @@ pub async fn handle_diff_set_files(args: &Value) -> Result<Value> {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or_else(|| anyhow::anyhow!("file_b is required"))?;
|
.ok_or_else(|| anyhow::anyhow!("file_b is required"))?;
|
||||||
|
|
||||||
let content_a = fs::read_to_string(file_a)?;
|
let content_a = read_file_as_utf8(file_a)?;
|
||||||
let content_b = fs::read_to_string(file_b)?;
|
let content_b = read_file_as_utf8(file_b)?;
|
||||||
|
|
||||||
let mut differences = Vec::new();
|
let mut differences = Vec::new();
|
||||||
|
|
||||||
@@ -223,20 +239,21 @@ pub async fn handle_describe_sweep(args: &Value) -> Result<Value> {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or_else(|| anyhow::anyhow!("path is required"))?;
|
.ok_or_else(|| anyhow::anyhow!("path is required"))?;
|
||||||
|
|
||||||
let content = fs::read_to_string(path)?;
|
let content = read_file_as_utf8(path)?;
|
||||||
let mut sweep_params = serde_json::Map::new();
|
let mut sweep_params = serde_json::Map::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
if let Some((key, value)) = line.split_once(':') {
|
if let Some((key, value)) = line.split_once('=') {
|
||||||
let key = key.trim();
|
let key = key.trim();
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
|
|
||||||
if value.contains("||Y") {
|
if value.contains("||Y") {
|
||||||
if let Some((from_val, to_val)) = value.split_once("..") {
|
let parts: Vec<&str> = value.split("||").collect();
|
||||||
|
if parts.len() >= 5 && parts[4].trim().to_uppercase() == "Y" {
|
||||||
sweep_params.insert(key.to_string(), json!({
|
sweep_params.insert(key.to_string(), json!({
|
||||||
"from": from_val.trim(),
|
"from": parts[1].trim(),
|
||||||
"to": to_val.trim().replace("||Y", ""),
|
"to": parts[3].trim(),
|
||||||
"step": 1.0
|
"step": parts[2].trim(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,7 +283,7 @@ pub async fn handle_list_set_files(config: &Config) -> Result<Value> {
|
|||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let content = fs::read_to_string(&path).unwrap_or_default();
|
let content = read_file_as_utf8(&path.to_string_lossy()).unwrap_or_default();
|
||||||
let param_count = content.lines().filter(|l| l.contains(':')).count();
|
let param_count = content.lines().filter(|l| l.contains(':')).count();
|
||||||
let sweep_count = content.lines().filter(|l| l.contains("||Y")).count();
|
let sweep_count = content.lines().filter(|l| l.contains("||Y")).count();
|
||||||
|
|
||||||
|
|||||||
+186
-64
@@ -2,9 +2,61 @@ use anyhow::{Context, Result};
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
use crate::models::Config;
|
use crate::models::Config;
|
||||||
use crate::storage::ReportDb;
|
use crate::storage::ReportDb;
|
||||||
|
|
||||||
|
/// OS detection and information
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
enum OsType {
|
||||||
|
Windows,
|
||||||
|
MacOS,
|
||||||
|
Linux,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OsType {
|
||||||
|
fn detect() -> Self {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
return OsType::Windows;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
return OsType::MacOS;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
return OsType::Linux;
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||||
|
return OsType::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
OsType::Windows => "windows",
|
||||||
|
OsType::MacOS => "macos",
|
||||||
|
OsType::Linux => "linux",
|
||||||
|
OsType::Unknown => "unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uses_wine(&self) -> bool {
|
||||||
|
matches!(self, OsType::MacOS | OsType::Linux)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect if using CrossOver instead of Wine on macOS
|
||||||
|
fn detect_crossover(wine_exe: &str) -> bool {
|
||||||
|
wine_exe.contains("crossover") || wine_exe.contains("CrossOver")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get OS context for diagnostic outputs
|
||||||
|
fn get_os_context() -> serde_json::Value {
|
||||||
|
let os_type = OsType::detect();
|
||||||
|
json!({
|
||||||
|
"os": os_type.as_str(),
|
||||||
|
"uses_wine": os_type.uses_wine(),
|
||||||
|
"architecture": std::env::consts::ARCH,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate that `user_path` resolves to a location within `allowed_base`.
|
/// Validate that `user_path` resolves to a location within `allowed_base`.
|
||||||
/// Returns the canonicalized absolute path on success.
|
/// Returns the canonicalized absolute path on success.
|
||||||
fn safe_output_path(user_path: &str, allowed_base: &Path) -> Result<PathBuf> {
|
fn safe_output_path(user_path: &str, allowed_base: &Path) -> Result<PathBuf> {
|
||||||
@@ -846,8 +898,11 @@ pub async fn handle_export_report(_config: &Config, args: &Value) -> Result<Valu
|
|||||||
|
|
||||||
/// Diagnose Wine installation and prefix health
|
/// Diagnose Wine installation and prefix health
|
||||||
pub async fn handle_diagnose_wine(config: &Config, _args: &Value) -> Result<Value> {
|
pub async fn handle_diagnose_wine(config: &Config, _args: &Value) -> Result<Value> {
|
||||||
|
let os_type = OsType::detect();
|
||||||
let mut diagnostics = json!({
|
let mut diagnostics = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"wine_executable": null,
|
"wine_executable": null,
|
||||||
|
"wine_type": null,
|
||||||
"wine_version": null,
|
"wine_version": null,
|
||||||
"wine_prefix": null,
|
"wine_prefix": null,
|
||||||
"prefix_health": null,
|
"prefix_health": null,
|
||||||
@@ -857,12 +912,16 @@ pub async fn handle_diagnose_wine(config: &Config, _args: &Value) -> Result<Valu
|
|||||||
"warnings": Vec::<String>::new(),
|
"warnings": Vec::<String>::new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check wine executable
|
// Check wine executable (macOS/Linux)
|
||||||
if let Some(wine_exe) = config.wine_executable.as_ref() {
|
if let Some(wine_exe) = config.wine_executable.as_ref() {
|
||||||
diagnostics["wine_executable"] = json!(wine_exe);
|
diagnostics["wine_executable"] = json!(wine_exe);
|
||||||
|
|
||||||
// Get Wine version
|
// Detect CrossOver vs Wine
|
||||||
let version_output = std::process::Command::new(wine_exe)
|
let is_crossover = detect_crossover(wine_exe);
|
||||||
|
diagnostics["wine_type"] = json!(if is_crossover { "crossover" } else { "wine" });
|
||||||
|
|
||||||
|
// Get Wine/CrossOver version
|
||||||
|
let version_output = Command::new(wine_exe)
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
@@ -872,14 +931,26 @@ pub async fn handle_diagnose_wine(config: &Config, _args: &Value) -> Result<Valu
|
|||||||
diagnostics["wine_version"] = json!(version);
|
diagnostics["wine_version"] = json!(version);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
let wine_type_str = if is_crossover { "CrossOver" } else { "Wine" };
|
||||||
diagnostics["errors"].as_array_mut().unwrap().push(
|
diagnostics["errors"].as_array_mut().unwrap().push(
|
||||||
json!("Failed to get Wine version - Wine may not be properly installed")
|
json!(format!("Failed to get {} version - may not be properly installed", wine_type_str))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS-specific CrossOver checks
|
||||||
|
if matches!(os_type, OsType::MacOS) && is_crossover {
|
||||||
|
// Check if CrossOver app exists
|
||||||
|
let crossover_app = Path::new("/Applications/CrossOver.app");
|
||||||
|
if !crossover_app.exists() {
|
||||||
|
diagnostics["warnings"].as_array_mut().unwrap().push(
|
||||||
|
json!("CrossOver.app not found in /Applications - may be custom installation")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
diagnostics["errors"].as_array_mut().unwrap().push(
|
diagnostics["errors"].as_array_mut().unwrap().push(
|
||||||
json!("Wine executable not configured")
|
json!("Wine/CrossOver executable not configured")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -991,6 +1062,7 @@ pub async fn handle_get_mt5_logs(config: &Config, args: &Value) -> Result<Value>
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"log_type": log_type,
|
"log_type": log_type,
|
||||||
"log_path": log_path.to_string_lossy().to_string(),
|
"log_path": log_path.to_string_lossy().to_string(),
|
||||||
"found": false,
|
"found": false,
|
||||||
@@ -1109,6 +1181,7 @@ pub async fn handle_search_mt5_errors(config: &Config, args: &Value) -> Result<V
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result = json!({
|
let result = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"hours_searched": hours_back,
|
"hours_searched": hours_back,
|
||||||
"errors_found": errors_found.len(),
|
"errors_found": errors_found.len(),
|
||||||
"max_errors": max_errors,
|
"max_errors": max_errors,
|
||||||
@@ -1128,16 +1201,17 @@ pub async fn handle_search_mt5_errors(config: &Config, args: &Value) -> Result<V
|
|||||||
|
|
||||||
/// Check MT5 process status
|
/// Check MT5 process status
|
||||||
pub async fn handle_check_mt5_process(_config: &Config, _args: &Value) -> Result<Value> {
|
pub async fn handle_check_mt5_process(_config: &Config, _args: &Value) -> Result<Value> {
|
||||||
use std::process::Command;
|
let _os_type = OsType::detect();
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"is_running": false,
|
"is_running": false,
|
||||||
"processes": Vec::<serde_json::Value>::new(),
|
"processes": Vec::<serde_json::Value>::new(),
|
||||||
"wine_server_running": false,
|
"wine_server_running": false,
|
||||||
"total_instances": 0,
|
"total_instances": 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
{
|
{
|
||||||
// Check for MT5 processes
|
// Check for MT5 processes
|
||||||
let ps_output = Command::new("ps")
|
let ps_output = Command::new("ps")
|
||||||
@@ -1149,6 +1223,7 @@ pub async fn handle_check_mt5_process(_config: &Config, _args: &Value) -> Result
|
|||||||
let mut processes = Vec::new();
|
let mut processes = Vec::new();
|
||||||
let mut mt5_count = 0;
|
let mut mt5_count = 0;
|
||||||
let mut wine_server = false;
|
let mut wine_server = false;
|
||||||
|
let mut crossover_server = false;
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let line_lower = line.to_lowercase();
|
let line_lower = line.to_lowercase();
|
||||||
@@ -1169,45 +1244,11 @@ pub async fn handle_check_mt5_process(_config: &Config, _args: &Value) -> Result
|
|||||||
if line_lower.contains("wineserver") {
|
if line_lower.contains("wineserver") {
|
||||||
wine_server = true;
|
wine_server = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
result["processes"] = json!(processes);
|
|
||||||
result["is_running"] = json!(mt5_count > 0);
|
|
||||||
result["total_instances"] = json!(mt5_count);
|
|
||||||
result["wine_server_running"] = json!(wine_server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let ps_output = Command::new("ps")
|
|
||||||
.args(["aux"])
|
|
||||||
.output();
|
|
||||||
|
|
||||||
if let Ok(output) = ps_output {
|
|
||||||
let content = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let mut processes = Vec::new();
|
|
||||||
let mut mt5_count = 0;
|
|
||||||
let mut wine_server = false;
|
|
||||||
|
|
||||||
for line in content.lines() {
|
|
||||||
let line_lower = line.to_lowercase();
|
|
||||||
|
|
||||||
if line_lower.contains("terminal64") || line_lower.contains("metatrader") {
|
// Detect CrossOver server on macOS
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
#[cfg(target_os = "macos")]
|
||||||
if parts.len() >= 11 {
|
if line_lower.contains("cxstart") || line_lower.contains("crossover") {
|
||||||
processes.push(json!({
|
crossover_server = true;
|
||||||
"pid": parts[1],
|
|
||||||
"cpu": parts[2],
|
|
||||||
"mem": parts[3],
|
|
||||||
"command": parts[10..].join(" "),
|
|
||||||
}));
|
|
||||||
mt5_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if line_lower.contains("wineserver") {
|
|
||||||
wine_server = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1215,6 +1256,11 @@ pub async fn handle_check_mt5_process(_config: &Config, _args: &Value) -> Result
|
|||||||
result["is_running"] = json!(mt5_count > 0);
|
result["is_running"] = json!(mt5_count > 0);
|
||||||
result["total_instances"] = json!(mt5_count);
|
result["total_instances"] = json!(mt5_count);
|
||||||
result["wine_server_running"] = json!(wine_server);
|
result["wine_server_running"] = json!(wine_server);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
result["crossover_server_running"] = json!(crossover_server);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,7 +1272,7 @@ pub async fn handle_check_mt5_process(_config: &Config, _args: &Value) -> Result
|
|||||||
|
|
||||||
/// Kill stuck MT5 process
|
/// Kill stuck MT5 process
|
||||||
pub async fn handle_kill_mt5_process(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_kill_mt5_process(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
use std::process::Command;
|
let _os_type = OsType::detect();
|
||||||
|
|
||||||
let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
|
let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
let pid = args.get("pid").and_then(|v| v.as_str());
|
let pid = args.get("pid").and_then(|v| v.as_str());
|
||||||
@@ -1271,6 +1317,12 @@ pub async fn handle_kill_mt5_process(_config: &Config, args: &Value) -> Result<V
|
|||||||
// Also kill wineserver if force=true
|
// Also kill wineserver if force=true
|
||||||
if force {
|
if force {
|
||||||
let _ = Command::new("killall").arg("wineserver").output();
|
let _ = Command::new("killall").arg("wineserver").output();
|
||||||
|
|
||||||
|
// On macOS, also kill CrossOver processes
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let _ = Command::new("killall").arg("cxstart").output();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1281,6 +1333,7 @@ pub async fn handle_kill_mt5_process(_config: &Config, args: &Value) -> Result<V
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = json!({
|
let result = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"killed": killed,
|
"killed": killed,
|
||||||
"failed": failed,
|
"failed": failed,
|
||||||
"force": force,
|
"force": force,
|
||||||
@@ -1295,9 +1348,10 @@ pub async fn handle_kill_mt5_process(_config: &Config, args: &Value) -> Result<V
|
|||||||
|
|
||||||
/// Check system resources for MT5
|
/// Check system resources for MT5
|
||||||
pub async fn handle_check_system_resources(_config: &Config, _args: &Value) -> Result<Value> {
|
pub async fn handle_check_system_resources(_config: &Config, _args: &Value) -> Result<Value> {
|
||||||
use std::process::Command;
|
let _os_type = OsType::detect();
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"disk_space": null,
|
"disk_space": null,
|
||||||
"memory": null,
|
"memory": null,
|
||||||
"cpu_cores": 0,
|
"cpu_cores": 0,
|
||||||
@@ -1345,34 +1399,66 @@ pub async fn handle_check_system_resources(_config: &Config, _args: &Value) -> R
|
|||||||
let vm_output = Command::new("vm_stat").output();
|
let vm_output = Command::new("vm_stat").output();
|
||||||
if let Ok(output) = vm_output {
|
if let Ok(output) = vm_output {
|
||||||
let content = String::from_utf8_lossy(&output.stdout);
|
let content = String::from_utf8_lossy(&output.stdout);
|
||||||
// Parse vm_stat output
|
// Parse vm_stat output with improved robustness
|
||||||
let mut free_pages = 0u64;
|
let mut free_pages = 0u64;
|
||||||
let mut active_pages = 0u64;
|
let mut active_pages = 0u64;
|
||||||
let mut inactive_pages = 0u64;
|
let mut inactive_pages = 0u64;
|
||||||
|
let mut wired_pages = 0u64;
|
||||||
|
let mut page_size = 4096u64;
|
||||||
|
|
||||||
|
// Try to get page size from vm_stat output
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
if line.contains("Pages free:") {
|
if line.contains("Mach Virtual Memory Statistics:") {
|
||||||
free_pages = line.split_whitespace().nth(2).unwrap_or("0").trim_end_matches('.').parse().unwrap_or(0);
|
// Page size is typically 4096 on macOS
|
||||||
} else if line.contains("Pages active:") {
|
page_size = 4096;
|
||||||
active_pages = line.split_whitespace().nth(2).unwrap_or("0").trim_end_matches('.').parse().unwrap_or(0);
|
|
||||||
} else if line.contains("Pages inactive:") {
|
|
||||||
inactive_pages = line.split_whitespace().nth(2).unwrap_or("0").trim_end_matches('.').parse().unwrap_or(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let page_size = 4096u64;
|
for line in content.lines() {
|
||||||
let total_mb = ((free_pages + active_pages + inactive_pages) * page_size) / 1024 / 1024;
|
// More robust parsing using regex-like pattern matching
|
||||||
|
if line.contains("Pages free:") {
|
||||||
|
if let Some(val) = line.split(':').nth(1) {
|
||||||
|
free_pages = val.trim().trim_end_matches('.').parse().unwrap_or(0);
|
||||||
|
}
|
||||||
|
} else if line.contains("Pages active:") {
|
||||||
|
if let Some(val) = line.split(':').nth(1) {
|
||||||
|
active_pages = val.trim().trim_end_matches('.').parse().unwrap_or(0);
|
||||||
|
}
|
||||||
|
} else if line.contains("Pages inactive:") {
|
||||||
|
if let Some(val) = line.split(':').nth(1) {
|
||||||
|
inactive_pages = val.trim().trim_end_matches('.').parse().unwrap_or(0);
|
||||||
|
}
|
||||||
|
} else if line.contains("Pages wired:") {
|
||||||
|
if let Some(val) = line.split(':').nth(1) {
|
||||||
|
wired_pages = val.trim().trim_end_matches('.').parse().unwrap_or(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate memory more accurately
|
||||||
|
let total_pages = free_pages + active_pages + inactive_pages + wired_pages;
|
||||||
|
let total_mb = (total_pages * page_size) / 1024 / 1024;
|
||||||
let free_mb = (free_pages * page_size) / 1024 / 1024;
|
let free_mb = (free_pages * page_size) / 1024 / 1024;
|
||||||
|
let available_mb = ((free_pages + inactive_pages) * page_size) / 1024 / 1024;
|
||||||
|
|
||||||
result["memory"] = json!({
|
result["memory"] = json!({
|
||||||
"total_mb": total_mb,
|
"total_mb": total_mb,
|
||||||
"free_mb": free_mb,
|
"free_mb": free_mb,
|
||||||
|
"available_mb": available_mb,
|
||||||
|
"active_mb": (active_pages * page_size) / 1024 / 1024,
|
||||||
|
"wired_mb": (wired_pages * page_size) / 1024 / 1024,
|
||||||
|
"page_size": page_size,
|
||||||
"unit": "MB",
|
"unit": "MB",
|
||||||
});
|
});
|
||||||
|
|
||||||
if free_mb < 2048 {
|
// Adjust memory threshold for macOS (compressed memory makes free appear lower)
|
||||||
|
if available_mb < 1024 {
|
||||||
result["recommendations"].as_array_mut().unwrap().push(
|
result["recommendations"].as_array_mut().unwrap().push(
|
||||||
json!("Low memory available. MT5 may crash during large optimizations.")
|
json!(format!("Low memory available ({} MB free). MT5 may crash during large optimizations. Close other apps.", available_mb))
|
||||||
|
);
|
||||||
|
} else if available_mb < 2048 {
|
||||||
|
result["recommendations"].as_array_mut().unwrap().push(
|
||||||
|
json!(format!("Memory getting low ({} MB available). Consider closing other applications.", available_mb))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1393,6 +1479,13 @@ pub async fn handle_check_system_resources(_config: &Config, _args: &Value) -> R
|
|||||||
"free_mb": parts[3].parse::<u64>().unwrap_or(0),
|
"free_mb": parts[3].parse::<u64>().unwrap_or(0),
|
||||||
"unit": "MB",
|
"unit": "MB",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let free_mb = parts[3].parse::<u64>().unwrap_or(0);
|
||||||
|
if free_mb < 1024 {
|
||||||
|
result["recommendations"].as_array_mut().unwrap().push(
|
||||||
|
json!("Low memory available. MT5 may crash during large optimizations.")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1400,13 +1493,38 @@ pub async fn handle_check_system_resources(_config: &Config, _args: &Value) -> R
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get CPU cores
|
// Get CPU cores
|
||||||
let nproc_output = Command::new("sysctl")
|
#[cfg(target_os = "macos")]
|
||||||
.args(["-n", "hw.ncpu"])
|
{
|
||||||
.output();
|
let nproc_output = Command::new("sysctl")
|
||||||
|
.args(["-n", "hw.ncpu"])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
if let Ok(output) = nproc_output {
|
||||||
|
let cores = String::from_utf8_lossy(&output.stdout).trim().parse().unwrap_or(0);
|
||||||
|
result["cpu_cores"] = json!(cores);
|
||||||
|
|
||||||
|
if cores < 4 {
|
||||||
|
result["recommendations"].as_array_mut().unwrap().push(
|
||||||
|
json!(format!("Low CPU core count ({} cores). Optimizations may be slow.", cores))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(output) = nproc_output {
|
#[cfg(target_os = "linux")]
|
||||||
let cores = String::from_utf8_lossy(&output.stdout).trim().parse().unwrap_or(0);
|
{
|
||||||
result["cpu_cores"] = json!(cores);
|
let nproc_output = Command::new("nproc").output();
|
||||||
|
|
||||||
|
if let Ok(output) = nproc_output {
|
||||||
|
let cores = String::from_utf8_lossy(&output.stdout).trim().parse().unwrap_or(0);
|
||||||
|
result["cpu_cores"] = json!(cores);
|
||||||
|
|
||||||
|
if cores < 4 {
|
||||||
|
result["recommendations"].as_array_mut().unwrap().push(
|
||||||
|
json!(format!("Low CPU core count ({} cores). Optimizations may be slow.", cores))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1422,6 +1540,7 @@ pub async fn handle_validate_mt5_config(config: &Config, _args: &Value) -> Resul
|
|||||||
.ok_or_else(|| anyhow::anyhow!("MT5 directory not configured"))?;
|
.ok_or_else(|| anyhow::anyhow!("MT5 directory not configured"))?;
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"terminal_ini": null,
|
"terminal_ini": null,
|
||||||
"tester_ini": null,
|
"tester_ini": null,
|
||||||
"config_files_found": Vec::<String>::new(),
|
"config_files_found": Vec::<String>::new(),
|
||||||
@@ -1505,6 +1624,7 @@ pub async fn handle_validate_mt5_config(config: &Config, _args: &Value) -> Resul
|
|||||||
|
|
||||||
/// Get Wine prefix detailed information
|
/// Get Wine prefix detailed information
|
||||||
pub async fn handle_get_wine_prefix_info(config: &Config, _args: &Value) -> Result<Value> {
|
pub async fn handle_get_wine_prefix_info(config: &Config, _args: &Value) -> Result<Value> {
|
||||||
|
let _os_type = OsType::detect();
|
||||||
let mt5_dir = config.mt5_dir()
|
let mt5_dir = config.mt5_dir()
|
||||||
.ok_or_else(|| anyhow::anyhow!("MT5 directory not configured"))?;
|
.ok_or_else(|| anyhow::anyhow!("MT5 directory not configured"))?;
|
||||||
|
|
||||||
@@ -1514,6 +1634,7 @@ pub async fn handle_get_wine_prefix_info(config: &Config, _args: &Value) -> Resu
|
|||||||
.and_then(|p| p.parent());
|
.and_then(|p| p.parent());
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"prefix_path": null,
|
"prefix_path": null,
|
||||||
"exists": false,
|
"exists": false,
|
||||||
"windows_version": null,
|
"windows_version": null,
|
||||||
@@ -1609,6 +1730,7 @@ pub async fn handle_get_backtest_crash_info(config: &Config, args: &Value) -> Re
|
|||||||
let hours_back = args.get("hours_back").and_then(|v| v.as_u64()).unwrap_or(6);
|
let hours_back = args.get("hours_back").and_then(|v| v.as_u64()).unwrap_or(6);
|
||||||
|
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
|
"os_context": get_os_context(),
|
||||||
"crashes_found": Vec::<serde_json::Value>::new(),
|
"crashes_found": Vec::<serde_json::Value>::new(),
|
||||||
"recent_failures": 0,
|
"recent_failures": 0,
|
||||||
"common_patterns": Vec::<String>::new(),
|
"common_patterns": Vec::<String>::new(),
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/// Read a file that may be UTF-16LE (with BOM) or UTF-8, returning a UTF-8 String.
|
||||||
|
/// MT5 .set and .ini files are typically UTF-16LE with BOM (0xFF 0xFE).
|
||||||
|
pub fn read_file_as_utf8(path: &std::path::Path) -> anyhow::Result<String> {
|
||||||
|
let bytes = std::fs::read(path)?;
|
||||||
|
|
||||||
|
// Check for UTF-16LE BOM (0xFF 0xFE)
|
||||||
|
if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE {
|
||||||
|
// UTF-16LE with BOM - skip the 2-byte BOM and decode
|
||||||
|
let utf16_data: Vec<u16> = bytes[2..]
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16(&utf16_data)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to decode UTF-16LE: {}", e))
|
||||||
|
} else {
|
||||||
|
// Try UTF-8
|
||||||
|
String::from_utf8(bytes)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to decode as UTF-8: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user