Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94a8c42e68 | ||
|
|
7e97086909 | ||
|
|
7a8a8daeca | ||
|
|
ba04405454 | ||
|
|
e40dbf63cf | ||
|
|
0a2e076ad6 | ||
|
|
8f71f1629d | ||
|
|
ad17f9dba6 | ||
|
|
33186d276a | ||
|
|
3927343999 | ||
|
|
6a5253186b | ||
|
|
76dac96583 | ||
|
|
0bc410f613 | ||
|
|
9be1296916 | ||
|
|
63c0470c6b |
@@ -0,0 +1,235 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version tag (e.g., v1.31.0)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Package binary
|
||||||
|
run: |
|
||||||
|
mkdir -p dist/mcp-mt5-quant-macos
|
||||||
|
cp target/release/mt5-quant dist/mcp-mt5-quant-macos/
|
||||||
|
cp -r config dist/mcp-mt5-quant-macos/
|
||||||
|
cp README.md dist/mcp-mt5-quant-macos/
|
||||||
|
cp -r docs dist/mcp-mt5-quant-macos/
|
||||||
|
cd dist
|
||||||
|
tar -czf mcp-mt5-quant-macos-arm64.tar.gz mcp-mt5-quant-macos
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-binary
|
||||||
|
path: dist/mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Package binary
|
||||||
|
run: |
|
||||||
|
mkdir -p dist/mcp-mt5-quant-linux
|
||||||
|
cp target/release/mt5-quant dist/mcp-mt5-quant-linux/
|
||||||
|
cp -r config dist/mcp-mt5-quant-linux/
|
||||||
|
cp README.md dist/mcp-mt5-quant-linux/
|
||||||
|
cp -r docs dist/mcp-mt5-quant-linux/
|
||||||
|
cd dist
|
||||||
|
tar -czf mcp-mt5-quant-linux-x64.tar.gz mcp-mt5-quant-linux
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-binary
|
||||||
|
path: dist/mcp-mt5-quant-linux-x64.tar.gz
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [build-macos, build-linux]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download macOS artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-binary
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Download Linux artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-binary
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Create or Update GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.event.inputs.version || github.ref_name }}
|
||||||
|
name: ${{ github.event.inputs.version || github.ref_name }}
|
||||||
|
files: |
|
||||||
|
dist/mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
|
dist/mcp-mt5-quant-linux-x64.tar.gz
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
generate_release_notes: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
build-mcp-package:
|
||||||
|
needs: release
|
||||||
|
runs-on: macos-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download macOS artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-binary
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Build MCP package
|
||||||
|
run: |
|
||||||
|
# Create MCP package structure
|
||||||
|
mkdir -p mcp-package/mcp-server/bin
|
||||||
|
|
||||||
|
# Extract binary from the tar.gz
|
||||||
|
cd dist
|
||||||
|
tar -xzf mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
|
cp mcp-mt5-quant-macos/mt5-quant ../mcp-package/mcp-server/bin/
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Copy config and docs
|
||||||
|
cp -r config mcp-package/
|
||||||
|
cp -r docs mcp-package/
|
||||||
|
cp README.md mcp-package/
|
||||||
|
cp server.json mcp-package/
|
||||||
|
|
||||||
|
# Create tar.gz
|
||||||
|
cd mcp-package
|
||||||
|
tar -czf ../mcp-mt5-quant-macos-arm64.tar.gz .
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Calculate SHA256
|
||||||
|
shasum -a 256 mcp-mt5-quant-macos-arm64.tar.gz | awk '{print $1}' > mcp-sha256.txt
|
||||||
|
echo "MCP Package SHA256: $(cat mcp-sha256.txt)"
|
||||||
|
|
||||||
|
- name: Upload MCP package to release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.event.inputs.version || github.ref_name }}
|
||||||
|
files: |
|
||||||
|
mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload MCP package artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mcp-package
|
||||||
|
path: mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
|
|
||||||
|
mcp-publish:
|
||||||
|
needs: build-mcp-package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write # Required for GitHub OIDC authentication
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download MCP package
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mcp-package
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Install mcp-publisher
|
||||||
|
run: |
|
||||||
|
# Download mcp-publisher from correct registry repository
|
||||||
|
curl -fsSL -o mcp-publisher.tar.gz "https://github.com/modelcontextprotocol/registry/releases/download/v1.0.0/mcp-publisher_1.0.0_linux_amd64.tar.gz" || {
|
||||||
|
echo "⚠️ Failed to download mcp-publisher - MCP publish will be skipped"
|
||||||
|
echo "Install manually: https://github.com/modelcontextprotocol/registry/releases"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
tar -xzf mcp-publisher.tar.gz mcp-publisher
|
||||||
|
chmod +x mcp-publisher
|
||||||
|
sudo mv mcp-publisher /usr/local/bin/
|
||||||
|
rm mcp-publisher.tar.gz
|
||||||
|
|
||||||
|
- name: Publish to MCP Registry
|
||||||
|
run: |
|
||||||
|
# Check if mcp-publisher is available
|
||||||
|
if ! command -v mcp-publisher &> /dev/null; then
|
||||||
|
echo "⚠️ mcp-publisher not found - skipping MCP publish"
|
||||||
|
echo "To publish manually:"
|
||||||
|
echo " 1. Install mcp-publisher:"
|
||||||
|
echo " curl -L \"https://github.com/modelcontextprotocol/registry/releases/download/v1.0.0/mcp-publisher_1.0.0_$(uname -s | tr '[:upper:]' '[:lower:]')_amd64.tar.gz\" | tar xz mcp-publisher"
|
||||||
|
echo " 2. Run: mcp-publisher login github"
|
||||||
|
echo " 3. Run: mcp-publisher publish"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== MCP Registry Publish ==="
|
||||||
|
# Use GitHub OIDC for authentication (no token needed)
|
||||||
|
mcp-publisher login github-oidc
|
||||||
|
|
||||||
|
# Publish the server
|
||||||
|
mcp-publisher publish
|
||||||
|
|
||||||
|
echo "✓ Published to MCP Registry"
|
||||||
|
|
||||||
|
release-info:
|
||||||
|
needs: [mcp-publish]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Display Release Commands
|
||||||
|
run: |
|
||||||
|
echo ""
|
||||||
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ 🎉 RELEASE COMPLETED SUCCESSFULLY! 🎉 ║"
|
||||||
|
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo "📦 GitHub Release: https://github.com/masdevid/mt5-quant/releases/tag/${{ github.event.inputs.version || github.ref_name }}"
|
||||||
|
echo "📦 MCP Registry: io.github.masdevid/mt5-quant"
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════════════════════════════"
|
||||||
|
echo " MCP CLIENT REGISTRATION COMMANDS"
|
||||||
|
echo "═══════════════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
echo "▶️ CLAUDE CODE:"
|
||||||
|
echo " claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant"
|
||||||
|
echo ""
|
||||||
|
echo "▶️ WINDSURF:"
|
||||||
|
echo ' echo \'{"mcpServers": {"io.github.masdevid/mt5-quant": {"command": "~/.local/bin/mt5-quant", "disabled": false, "registry": "io.github.masdevid/mt5-quant"}}}\' >> ~/.codeium/windsurf/mcp_config.json'
|
||||||
|
echo ""
|
||||||
|
echo "▶️ CURSOR / VSCODE:"
|
||||||
|
echo ' mkdir -p ~/.cursor && echo \'{"mcpServers": {"mt5-quant": {"command": "~/.local/bin/mt5-quant"}}}\' > ~/.cursor/mcp.json'
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════════════════════════════"
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
description: Release workflow - bump version, build, and publish MCP server
|
||||||
|
tags: [release, publish, workflow]
|
||||||
|
---
|
||||||
|
|
||||||
|
# MT5-Quant Release Workflow
|
||||||
|
|
||||||
|
Steps to release a new version of the MCP server.
|
||||||
|
|
||||||
|
## 1. Pre-Release Checklist
|
||||||
|
|
||||||
|
- [ ] All features implemented and tested
|
||||||
|
- [ ] Documentation updated (README.md, MCP_TOOLS.md)
|
||||||
|
- [ ] Tool count verified: `grep -r "pub fn tool_" src/tools/definitions/ | wc -l`
|
||||||
|
- [ ] Version bumped in Cargo.toml
|
||||||
|
- [ ] Build passes: `cargo build --release`
|
||||||
|
|
||||||
|
## 2. Update Documentation
|
||||||
|
|
||||||
|
// turbo
|
||||||
|
```bash
|
||||||
|
grep -r "pub fn tool_" src/tools/definitions/ | wc -l
|
||||||
|
```
|
||||||
|
|
||||||
|
Update these files with correct tool counts:
|
||||||
|
- `README.md` - header and "MCP Tools (N)" section
|
||||||
|
- `docs/MCP_TOOLS.md` - "documents X of Y total tools" line
|
||||||
|
|
||||||
|
## 3. Bump Version
|
||||||
|
|
||||||
|
Edit `Cargo.toml` and update the version:
|
||||||
|
```toml
|
||||||
|
version = "X.Y.Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Build Release
|
||||||
|
|
||||||
|
// turbo
|
||||||
|
```bash
|
||||||
|
bash scripts/build-release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates: `dist/mcp-mt5-quant-{platform}.tar.gz`
|
||||||
|
|
||||||
|
Calculate SHA256 for server.json:
|
||||||
|
```bash
|
||||||
|
shasum -a 256 dist/mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Update MCP Server Configuration
|
||||||
|
|
||||||
|
Update both `server.json` files with:
|
||||||
|
- New version
|
||||||
|
- New SHA256 hash from step 4
|
||||||
|
- Updated download URL
|
||||||
|
- Tool count in description
|
||||||
|
|
||||||
|
Files to update:
|
||||||
|
- `server.json`
|
||||||
|
- `mcp-package/server.json`
|
||||||
|
|
||||||
|
Then copy the release package:
|
||||||
|
```bash
|
||||||
|
cp dist/mcp-mt5-quant-macos-arm64.tar.gz mcp-package/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Create Git Tag & Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Release vX.Y.Z - brief description"
|
||||||
|
git tag vX.Y.Z
|
||||||
|
git push origin vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Create GitHub Release (Manual)
|
||||||
|
|
||||||
|
1. Go to GitHub → Releases → Draft new release
|
||||||
|
2. Choose tag: `vX.Y.Z`
|
||||||
|
3. Release title: `vX.Y.Z`
|
||||||
|
4. Attach binary: `dist/mcp-mt5-quant-macos-arm64.tar.gz`
|
||||||
|
5. Publish release
|
||||||
|
|
||||||
|
## 8. Install Binary to System Path
|
||||||
|
|
||||||
|
Install the binary to a single location for all MCP clients:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create local bin if needed
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
|
||||||
|
# Install binary
|
||||||
|
cp target/release/mt5-quant ~/.local/bin/
|
||||||
|
|
||||||
|
# Or system-wide (requires sudo)
|
||||||
|
# sudo cp target/release/mt5-quant /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Installation Path:** `~/.local/bin/mt5-quant` (single location for all clients)
|
||||||
|
|
||||||
|
## 9. MCP Registration
|
||||||
|
|
||||||
|
Use the installed binary path (not project directory):
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
```bash
|
||||||
|
claude mcp add mt5-quant -- ~/.local/bin/mt5-quant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windsurf
|
||||||
|
Edit `~/.codeium/windsurf/mcp_config.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mt5-quant": {
|
||||||
|
"command": "~/.local/bin/mt5-quant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code / Cursor
|
||||||
|
Edit `~/.cursor/mcp.json` or `.vscode/mcp.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mt5-quant": {
|
||||||
|
"command": "~/.local/bin/mt5-quant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. MCP Client Registration
|
||||||
|
|
||||||
|
After installing the binary, register with your MCP clients:
|
||||||
|
|
||||||
|
### Windsurf
|
||||||
|
|
||||||
|
// turbo
|
||||||
|
```bash
|
||||||
|
# Add to Windsurf MCP config
|
||||||
|
cat >> ~/.codeium/windsurf/mcp_config.json << 'EOF'
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"io.github.masdevid/mt5-quant": {
|
||||||
|
"command": "~/.local/bin/mt5-quant",
|
||||||
|
"disabled": false,
|
||||||
|
"registry": "io.github.masdevid/mt5-quant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
|
||||||
|
// turbo
|
||||||
|
```bash
|
||||||
|
# Register with Claude Code
|
||||||
|
claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant
|
||||||
|
|
||||||
|
# Or with custom name
|
||||||
|
claude mcp add mt5-quant -- ~/.local/bin/mt5-quant
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code / Cursor
|
||||||
|
|
||||||
|
// turbo
|
||||||
|
```bash
|
||||||
|
# Add to Cursor MCP config
|
||||||
|
mkdir -p ~/.cursor
|
||||||
|
cat > ~/.cursor/mcp.json << 'EOF'
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mt5-quant": {
|
||||||
|
"command": "~/.local/bin/mt5-quant",
|
||||||
|
"env": {
|
||||||
|
"MT5_MCP_HOME": "~/.config/mt5-quant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Post-Release Verification
|
||||||
|
|
||||||
|
// turbo
|
||||||
|
```bash
|
||||||
|
# Test the binary
|
||||||
|
./target/release/mt5-quant --help
|
||||||
|
|
||||||
|
# Verify tool count
|
||||||
|
./target/release/mt5-quant 2>&1 | head -20
|
||||||
|
|
||||||
|
# Check MCP registration
|
||||||
|
claude mcp list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Release Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full release cycle
|
||||||
|
vim Cargo.toml # bump version
|
||||||
|
bash scripts/build-release.sh
|
||||||
|
git add . && git commit -m "Release vX.Y.Z"
|
||||||
|
git tag vX.Y.Z && git push origin vX.Y.Z
|
||||||
|
```
|
||||||
Generated
+1
-1
@@ -481,7 +481,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mt5-quant"
|
name = "mt5-quant"
|
||||||
version = "1.30.0"
|
version = "1.31.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mt5-quant"
|
name = "mt5-quant"
|
||||||
version = "1.30.0"
|
version = "1.31.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "MT5-Quant MCP Server - Exposes MT5 backtest and optimization tools via MCP"
|
description = "MT5-Quant MCP Server - Exposes MT5 backtest and optimization tools via MCP"
|
||||||
authors = ["masdevid <masdevid@example.com>"]
|
authors = ["masdevid <masdevid@example.com>"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MT5-Quant
|
# MT5-Quant
|
||||||
|
|
||||||
**MCP server for MT5 strategy development on macOS/Linux.** 85 tools to compile, backtest, analyze, optimize, debug crashes, and manage MQL5 Expert Advisors — no Windows required.
|
**MCP server for MT5 strategy development on macOS/Linux.** 87 tools to compile, backtest, analyze, optimize, debug crashes, and manage MQL5 Expert Advisors — no Windows required.
|
||||||
|
|
||||||
```
|
```
|
||||||
You: "Backtest MyEA Jan-Mar, what caused the February drawdown?"
|
You: "Backtest MyEA Jan-Mar, what caused the February drawdown?"
|
||||||
@@ -62,18 +62,22 @@ The AI runs the full pipeline: compile → clean cache → backtest → extract
|
|||||||
| [VSCODE.md](docs/VSCODE.md) | VS Code setup |
|
| [VSCODE.md](docs/VSCODE.md) | VS Code setup |
|
||||||
| [ANTIGRAVITY.md](docs/ANTIGRAVITY.md) | Antigravity IDE setup |
|
| [ANTIGRAVITY.md](docs/ANTIGRAVITY.md) | Antigravity IDE setup |
|
||||||
| [CONFIG.md](docs/CONFIG.md) | Configuration reference |
|
| [CONFIG.md](docs/CONFIG.md) | Configuration reference |
|
||||||
| [TOOLS.md](docs/MCP_TOOLS.md) | All 75 tools documented |
|
| [TOOLS.md](docs/MCP_TOOLS.md) | All 87 tools documented |
|
||||||
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design and internals |
|
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design and internals |
|
||||||
| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common issues |
|
| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common issues |
|
||||||
| [REMOTE_AGENTS.md](docs/REMOTE_AGENTS.md) | Linux optimization agents |
|
| [REMOTE_AGENTS.md](docs/REMOTE_AGENTS.md) | Linux optimization agents |
|
||||||
|
|
||||||
## MCP Tools (75)
|
## MCP Tools (87)
|
||||||
|
|
||||||
### 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_only` | Backtest only - just extract raw trades, no analysis |
|
||||||
|
| `launch_backtest` | Fire-and-forget: launch MT5 backtest, poll for completion |
|
||||||
|
| `get_backtest_status` | Poll running backtest status (MT5 running, report found, elapsed time) |
|
||||||
| `run_optimization` | Genetic optimization (background, returns immediately) |
|
| `run_optimization` | Genetic optimization (background, returns immediately) |
|
||||||
| `get_optimization_results` | Parse optimization results after MT5 finishes |
|
| `get_optimization_results` | Parse optimization results after MT5 finishes |
|
||||||
| `analyze_report` | Read `analysis.json` from any report directory |
|
| `analyze_report` | Read `analysis.json` from any report directory |
|
||||||
@@ -119,7 +123,6 @@ Use these for targeted analysis, or `analyze_report` to run all at once.
|
|||||||
| 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_backtest_status` | Check live progress of a running backtest pipeline |
|
|
||||||
| `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 |
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -29,7 +29,7 @@ cargo build --release
|
|||||||
3. Click **Add Custom MCP**
|
3. Click **Add Custom MCP**
|
||||||
4. Enter:
|
4. Enter:
|
||||||
- **Name**: `mt5-quant`
|
- **Name**: `mt5-quant`
|
||||||
- **Command**: Full path to binary (e.g., `/Users/name/mt5-quant/target/release/mt5-quant`)
|
- **Command**: `~/.local/bin/mt5-quant`
|
||||||
- **Type**: `stdio`
|
- **Type**: `stdio`
|
||||||
|
|
||||||
### Method 2: Edit mcp.json Directly
|
### Method 2: Edit mcp.json Directly
|
||||||
@@ -41,7 +41,7 @@ Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
|
|||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mt5-quant": {
|
"mt5-quant": {
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"command": "/absolute/path/to/mt5-quant"
|
"command": "~/.local/bin/mt5-quant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Full input/output schemas for MT5-Quant tools.
|
Full input/output schemas for MT5-Quant tools.
|
||||||
|
|
||||||
> **Documentation Status:** This file documents 49 of 85 total tools. Missing:
|
> **Documentation Status:** This file documents 56 of 87 total tools. Missing:
|
||||||
> - `list_experts`, `list_indicators`, `list_scripts`
|
> - `list_experts`, `list_indicators`, `list_scripts`
|
||||||
> - `healthcheck`, `list_symbols`
|
> - `healthcheck`, `list_symbols`
|
||||||
> - Reports query: `search_reports`, `get_latest_report`, `list_reports`, `prune_reports`, `tail_log`, `get_report_by_id`, `get_reports_summary`, `get_best_reports`, `search_reports_by_tags`, `search_reports_by_date_range`, `search_reports_by_notes`, `get_reports_by_set_file`, `get_comparable_reports`
|
> - Reports query: `search_reports`, `get_latest_report`, `list_reports`, `prune_reports`, `tail_log`, `get_report_by_id`, `get_reports_summary`, `get_best_reports`, `search_reports_by_tags`, `search_reports_by_date_range`, `search_reports_by_notes`, `get_reports_by_set_file`, `get_comparable_reports`
|
||||||
@@ -145,6 +145,124 @@ Run a complete backtest pipeline: compile → clean cache → backtest → extra
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## `run_backtest_quick`
|
||||||
|
|
||||||
|
Quick backtest using pre-compiled EA: clean cache → backtest → extract → analyze.
|
||||||
|
|
||||||
|
**When to call:** When EA code hasn't changed and you just want to test different parameters or date ranges. Faster than `run_backtest` because it skips compilation.
|
||||||
|
|
||||||
|
### Input schema
|
||||||
|
|
||||||
|
Same as `run_backtest`, but `skip_compile` is automatically set to `true`.
|
||||||
|
|
||||||
|
### Output schema
|
||||||
|
|
||||||
|
Same as `run_backtest`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `run_backtest_only`
|
||||||
|
|
||||||
|
Backtest only: clean cache → backtest → extract. No analysis phase.
|
||||||
|
|
||||||
|
**When to call:** When you just need raw trade data (deals.csv) and don't need analytics. Fastest option for batch processing.
|
||||||
|
|
||||||
|
### Input schema
|
||||||
|
|
||||||
|
Same as `run_backtest`, but `skip_compile` and `skip_analyze` are automatically set to `true`.
|
||||||
|
|
||||||
|
### Output schema
|
||||||
|
|
||||||
|
Same as `run_backtest` but without `analysis_summary`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `launch_backtest`
|
||||||
|
|
||||||
|
Fire-and-forget mode: compile → clean → launch MT5 backtest, return immediately with job info.
|
||||||
|
|
||||||
|
**When to call:** When you want to launch a backtest without waiting for completion. Use `get_backtest_status` to poll for completion.
|
||||||
|
|
||||||
|
### Input schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
expert: string; // Required. EA name without path or extension
|
||||||
|
symbol?: string; // Trading symbol (default: from config or first available)
|
||||||
|
from_date?: string; // Start date YYYY.MM.DD (default: past complete month)
|
||||||
|
to_date?: string; // End date YYYY.MM.DD (default: past complete month)
|
||||||
|
timeframe?: string; // M1, M5, M15, M30, H1, H4, D1 (default: M5)
|
||||||
|
deposit?: number; // Initial deposit (default: 10000)
|
||||||
|
model?: 0 | 1 | 2; // Tick model (default: 0)
|
||||||
|
set_file?: string; // Path to .set parameter file
|
||||||
|
skip_compile?: boolean; // Skip compilation
|
||||||
|
skip_clean?: boolean; // Skip cache cleaning
|
||||||
|
timeout?: number; // Max time in seconds (default: 900)
|
||||||
|
gui?: boolean; // Enable MT5 visualization
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: true;
|
||||||
|
message: string; // "Backtest launched successfully..."
|
||||||
|
report_id: string; // e.g., "20250122_034455_MyEA_XAUUSD_M5"
|
||||||
|
report_dir: string; // Full path to report directory
|
||||||
|
expert: string;
|
||||||
|
symbol: string;
|
||||||
|
timeframe: string;
|
||||||
|
launched_at: string; // ISO8601 timestamp
|
||||||
|
timeout_seconds: number;
|
||||||
|
poll_hint: string; // "Call get_backtest_status with report_dir to check progress"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `get_backtest_status`
|
||||||
|
|
||||||
|
Check progress of a running backtest pipeline launched via `launch_backtest`.
|
||||||
|
|
||||||
|
**When to call:** Poll periodically after calling `launch_backtest` to check completion status.
|
||||||
|
|
||||||
|
### Input schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
report_dir: string; // Report directory path from launch_backtest output
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: true;
|
||||||
|
report_dir: string;
|
||||||
|
status: "completed" | "running" | "failed" | "in_progress" | "not_started";
|
||||||
|
stage: string; // Current pipeline stage: COMPILE, CLEAN, BACKTEST, EXTRACT, ANALYZE, DONE
|
||||||
|
is_complete: boolean; // True if backtest finished successfully
|
||||||
|
mt5_running: boolean; // Whether MT5 process is active
|
||||||
|
report_found: boolean; // Whether report file exists
|
||||||
|
metrics_extracted: boolean;
|
||||||
|
deals_extracted: boolean;
|
||||||
|
elapsed_seconds: number; // Time since launch
|
||||||
|
message: string; // Human-readable status message
|
||||||
|
job?: {
|
||||||
|
report_id: string;
|
||||||
|
expert: string;
|
||||||
|
symbol: string;
|
||||||
|
timeframe: string;
|
||||||
|
launched_at: string;
|
||||||
|
timeout_seconds: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## `run_optimization`
|
## `run_optimization`
|
||||||
|
|
||||||
Launch genetic parameter optimization as a detached background process.
|
Launch genetic parameter optimization as a detached background process.
|
||||||
|
|||||||
+6
-4
@@ -81,7 +81,7 @@ terminal_dir: "~/Library/Application Support/net.metaquotes.wine.metatrader5/dri
|
|||||||
### Claude Code
|
### Claude Code
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude mcp add MT5-Quant -- /path/to/mt5-quant/target/release/mt5-quant
|
claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify:
|
Verify:
|
||||||
@@ -96,8 +96,10 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mt5-quant": {
|
"io.github.masdevid/mt5-quant": {
|
||||||
"command": "/path/to/mt5-quant"
|
"command": "~/.local/bin/mt5-quant",
|
||||||
|
"disabled": false,
|
||||||
|
"registry": "io.github.masdevid/mt5-quant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +113,7 @@ Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mt5-quant": {
|
"mt5-quant": {
|
||||||
"command": "/path/to/mt5-quant"
|
"command": "~/.local/bin/mt5-quant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -28,7 +28,7 @@ cargo build --release
|
|||||||
2. Run `MCP: Add Server`
|
2. Run `MCP: Add Server`
|
||||||
3. Choose **Workspace** or **User** scope
|
3. Choose **Workspace** or **User** scope
|
||||||
4. Enter server name: `mt5-quant`
|
4. Enter server name: `mt5-quant`
|
||||||
5. Enter command: Full path to binary (e.g., `/Users/name/mt5-quant/target/release/mt5-quant`)
|
5. Enter command: `~/.local/bin/mt5-quant`
|
||||||
|
|
||||||
### Method 2: Edit mcp.json Directly
|
### Method 2: Edit mcp.json Directly
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ Add to `.vscode/mcp.json` in your workspace:
|
|||||||
{
|
{
|
||||||
"servers": {
|
"servers": {
|
||||||
"mt5-quant": {
|
"mt5-quant": {
|
||||||
"command": "/absolute/path/to/mt5-quant"
|
"command": "~/.local/bin/mt5-quant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ cat > .vscode/mcp.json << 'EOF'
|
|||||||
{
|
{
|
||||||
"servers": {
|
"servers": {
|
||||||
"mt5-quant": {
|
"mt5-quant": {
|
||||||
"command": "/path/to/mt5-quant"
|
"command": "~/.local/bin/mt5-quant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ EOF
|
|||||||
### Method 3: VS Code CLI
|
### Method 3: VS Code CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
code --add-mcp '{"name":"mt5-quant","command":"/path/to/mt5-quant"}'
|
code --add-mcp '{"name":"mt5-quant","command":"~/.local/bin/mt5-quant"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Verify Setup
|
## Verify Setup
|
||||||
|
|||||||
+8
-2
@@ -27,8 +27,10 @@ Edit `~/.codeium/windsurf/mcp_config.json`:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mt5-quant": {
|
"io.github.masdevid/mt5-quant": {
|
||||||
"command": "/Users/masdevid/jobs/mt5-quant/target/release/mt5-quant"
|
"command": "~/.local/bin/mt5-quant",
|
||||||
|
"disabled": false,
|
||||||
|
"registry": "io.github.masdevid/mt5-quant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +39,10 @@ Edit `~/.codeium/windsurf/mcp_config.json`:
|
|||||||
Or use the automated setup:
|
Or use the automated setup:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Install binary to standard location
|
||||||
|
cp target/release/mt5-quant ~/.local/bin/
|
||||||
|
|
||||||
|
# Then configure
|
||||||
bash scripts/setup.sh
|
bash scripts/setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+15
-7
@@ -1024,14 +1024,20 @@ _register_with_platform() {
|
|||||||
local platform="$1"
|
local platform="$1"
|
||||||
local use_binary="${2:-false}"
|
local use_binary="${2:-false}"
|
||||||
|
|
||||||
# Determine command path (binary for Windsurf/Cursor, Python for Claude)
|
# Determine command path - use standard installation location
|
||||||
local cmd_path
|
local cmd_path
|
||||||
if $use_binary && [[ -f "${REPO_DIR}/target/release/mt5-quant" ]]; then
|
local default_install="$HOME/.local/bin/mt5-quant"
|
||||||
|
|
||||||
|
if [[ -f "$default_install" ]]; then
|
||||||
|
cmd_path="$default_install"
|
||||||
|
elif [[ -f "${REPO_DIR}/target/release/mt5-quant" ]]; then
|
||||||
|
# Fallback to project build if standard location not found
|
||||||
cmd_path="${REPO_DIR}/target/release/mt5-quant"
|
cmd_path="${REPO_DIR}/target/release/mt5-quant"
|
||||||
elif [[ -f "${REPO_DIR}/mt5-quant" ]]; then
|
_warn "Using project build at $cmd_path"
|
||||||
cmd_path="${REPO_DIR}/mt5-quant"
|
_warn "Consider installing to standard location: cp $cmd_path ~/.local/bin/"
|
||||||
else
|
else
|
||||||
cmd_path="python3 ${REPO_DIR}/server/main.py"
|
_fail "mt5-quant not found at $default_install or ${REPO_DIR}/target/release/"
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$platform" in
|
case "$platform" in
|
||||||
@@ -1066,8 +1072,10 @@ if os.path.exists(config_path):
|
|||||||
if 'mcpServers' not in data:
|
if 'mcpServers' not in data:
|
||||||
data['mcpServers'] = {}
|
data['mcpServers'] = {}
|
||||||
|
|
||||||
data['mcpServers']['mt5-quant'] = {
|
data['mcpServers']['io.github.masdevid/mt5-quant'] = {
|
||||||
'command': '$cmd_path'
|
'command': '$cmd_path',
|
||||||
|
'disabled': False,
|
||||||
|
'registry': 'io.github.masdevid/mt5-quant'
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(config_path, 'w') as f:
|
with open(config_path, 'w') as f:
|
||||||
|
|||||||
+6
-6
@@ -2,18 +2,18 @@
|
|||||||
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
||||||
"name": "io.github.masdevid/mt5-quant",
|
"name": "io.github.masdevid/mt5-quant",
|
||||||
"title": "MT5-Quant",
|
"title": "MT5-Quant",
|
||||||
"description": "MCP server for MT5 strategy development with 85 tools. Compile, backtest, analyze, optimize, and debug MQL5 EAs on macOS/Linux. Includes Wine/MT5 crash diagnostics.",
|
"description": "MT5 strategy development with 87 tools. Backtest, optimize, debug MQL5 EAs on macOS/Linux.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://github.com/masdevid/mt5-quant",
|
"url": "https://github.com/masdevid/mt5-quant",
|
||||||
"source": "github"
|
"source": "github"
|
||||||
},
|
},
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"registryType": "mcpb",
|
"registryType": "mcpbPackageType",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.30.0/mcp-mt5-quant-macos-arm64.tar.gz",
|
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.31.0/mcp-mt5-quant-macos-arm64.tar.gz",
|
||||||
"fileSha256": "PLACEHOLDER_SHA256",
|
"fileSha256": "b23ab29823c14bc2c968869ec9ce0787e60fe531f40f19e6492d0b3314f8223d",
|
||||||
"transport": {
|
"transport": {
|
||||||
"type": "stdio"
|
"type": "stdio"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::time::Duration;
|
||||||
|
use tokio::time::timeout as tokio_timeout;
|
||||||
|
|
||||||
use crate::models::Config;
|
use crate::models::Config;
|
||||||
|
|
||||||
@@ -28,7 +29,11 @@ impl MqlCompiler {
|
|||||||
Self { config }
|
Self { config }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile(&self, source_path: &str) -> Result<CompileResult> {
|
pub async fn compile(&self, source_path: &str) -> Result<CompileResult> {
|
||||||
|
self.compile_with_timeout(source_path, Duration::from_secs(120)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn compile_with_timeout(&self, source_path: &str, timeout: Duration) -> Result<CompileResult> {
|
||||||
let source_path = Path::new(source_path);
|
let source_path = Path::new(source_path);
|
||||||
if !source_path.exists() {
|
if !source_path.exists() {
|
||||||
return Err(anyhow!("Source file not found: {}", source_path.display()));
|
return Err(anyhow!("Source file not found: {}", source_path.display()));
|
||||||
@@ -61,7 +66,7 @@ impl MqlCompiler {
|
|||||||
let staged_mq5 = &sync.dest_mq5;
|
let staged_mq5 = &sync.dest_mq5;
|
||||||
tracing::info!("Staged {} file(s) to: {}", sync.files_copied, staged_mq5.display());
|
tracing::info!("Staged {} file(s) to: {}", sync.files_copied, staged_mq5.display());
|
||||||
|
|
||||||
self.run_metaeditor(wine_exe, &wine_prefix, &metaeditor, staged_mq5)?;
|
self.run_metaeditor_with_timeout(wine_exe, &wine_prefix, &metaeditor, staged_mq5, timeout).await?;
|
||||||
|
|
||||||
// /log flag (no path) writes log adjacent to source: {ea_name}.log
|
// /log flag (no path) writes log adjacent to source: {ea_name}.log
|
||||||
let log_path = staged_mq5.with_extension("log");
|
let log_path = staged_mq5.with_extension("log");
|
||||||
@@ -197,15 +202,16 @@ impl MqlCompiler {
|
|||||||
Ok(SyncStats { dest_mq5, files_copied })
|
Ok(SyncStats { dest_mq5, files_copied })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run MetaEditor to compile `source_mq5`.
|
/// Run MetaEditor to compile `source_mq5` with timeout.
|
||||||
/// Uses Unix host path for /compile: and bare /log flag (writes log adjacent to source).
|
/// Uses Unix host path for /compile: and bare /log flag (writes log adjacent to source).
|
||||||
/// Shell script intermediary required on macOS to preserve DYLD_* vars past SIP.
|
/// Shell script intermediary required on macOS to preserve DYLD_* vars past SIP.
|
||||||
fn run_metaeditor(
|
async fn run_metaeditor_with_timeout(
|
||||||
&self,
|
&self,
|
||||||
wine_exe: &str,
|
wine_exe: &str,
|
||||||
wine_prefix: &Path,
|
wine_prefix: &Path,
|
||||||
metaeditor: &Path,
|
metaeditor: &Path,
|
||||||
source_mq5: &Path,
|
source_mq5: &Path,
|
||||||
|
timeout: Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mt5_dir = metaeditor.parent().unwrap_or(metaeditor);
|
let mt5_dir = metaeditor.parent().unwrap_or(metaeditor);
|
||||||
|
|
||||||
@@ -242,16 +248,24 @@ impl MqlCompiler {
|
|||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
|
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
|
||||||
}
|
}
|
||||||
Command::new("/bin/sh").arg(&script_path).output()?;
|
let compile_future = tokio::process::Command::new("/bin/sh")
|
||||||
|
.arg(&script_path)
|
||||||
|
.output();
|
||||||
|
let result = tokio_timeout(timeout, compile_future).await
|
||||||
|
.map_err(|_| anyhow!("Compilation timed out after {} seconds", timeout.as_secs()))?;
|
||||||
|
result?;
|
||||||
} else {
|
} else {
|
||||||
Command::new(wine_exe)
|
let compile_future = tokio::process::Command::new(wine_exe)
|
||||||
.arg(metaeditor)
|
.arg(metaeditor)
|
||||||
.arg(format!("/compile:{}", source_mq5.display()))
|
.arg(format!("/compile:{}", source_mq5.display()))
|
||||||
.arg("/log")
|
.arg("/log")
|
||||||
.env("WINEPREFIX", wine_prefix)
|
.env("WINEPREFIX", wine_prefix)
|
||||||
.env("WINEDEBUG", "-all")
|
.env("WINEDEBUG", "-all")
|
||||||
.current_dir(mt5_dir)
|
.current_dir(mt5_dir)
|
||||||
.output()?;
|
.output();
|
||||||
|
let result = tokio_timeout(timeout, compile_future).await
|
||||||
|
.map_err(|_| anyhow!("Compilation timed out after {} seconds", timeout.as_secs()))?;
|
||||||
|
result?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// Re-export chrono for BacktestJob
|
||||||
|
use chrono;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Report {
|
pub struct Report {
|
||||||
@@ -31,6 +34,8 @@ pub struct PipelineMetadata {
|
|||||||
pub report_dir: String,
|
pub report_dir: String,
|
||||||
pub duration_seconds: i64,
|
pub duration_seconds: i64,
|
||||||
pub files: FilePaths,
|
pub files: FilePaths,
|
||||||
|
#[serde(default)]
|
||||||
|
pub no_trades: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -48,6 +53,9 @@ pub struct BacktestStatus {
|
|||||||
pub elapsed_seconds: i64,
|
pub elapsed_seconds: i64,
|
||||||
pub is_complete: bool,
|
pub is_complete: bool,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
pub report_dir: Option<String>,
|
||||||
|
pub mt5_running: Option<bool>,
|
||||||
|
pub report_found: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -60,4 +68,43 @@ pub enum PipelineStage {
|
|||||||
Extract,
|
Extract,
|
||||||
Analyze,
|
Analyze,
|
||||||
Done,
|
Done,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track a running backtest job for fire-and-poll pattern
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BacktestJob {
|
||||||
|
pub report_id: String,
|
||||||
|
pub report_dir: String,
|
||||||
|
pub expert: String,
|
||||||
|
pub symbol: String,
|
||||||
|
pub timeframe: String,
|
||||||
|
pub launched_at: String,
|
||||||
|
pub mt5_pid: Option<u32>,
|
||||||
|
pub expected_report_path: String,
|
||||||
|
pub timeout_seconds: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BacktestJob {
|
||||||
|
pub fn new(
|
||||||
|
report_id: String,
|
||||||
|
report_dir: String,
|
||||||
|
expert: String,
|
||||||
|
symbol: String,
|
||||||
|
timeframe: String,
|
||||||
|
expected_report_path: String,
|
||||||
|
timeout_seconds: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
report_id,
|
||||||
|
report_dir,
|
||||||
|
expert,
|
||||||
|
symbol,
|
||||||
|
timeframe,
|
||||||
|
launched_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
mt5_pid: None,
|
||||||
|
expected_report_path,
|
||||||
|
timeout_seconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-7
@@ -8,7 +8,7 @@ use tokio::time::{sleep, Duration};
|
|||||||
use crate::analytics::{DealAnalyzer, ReportExtractor};
|
use crate::analytics::{DealAnalyzer, ReportExtractor};
|
||||||
use crate::compile::MqlCompiler;
|
use crate::compile::MqlCompiler;
|
||||||
use crate::models::config::Config;
|
use crate::models::config::Config;
|
||||||
use crate::models::report::{PipelineMetadata, FilePaths};
|
use crate::models::report::{PipelineMetadata, FilePaths, BacktestJob};
|
||||||
use crate::storage::{ReportDb, ReportEntry};
|
use crate::storage::{ReportDb, ReportEntry};
|
||||||
|
|
||||||
pub struct BacktestPipeline {
|
pub struct BacktestPipeline {
|
||||||
@@ -73,7 +73,7 @@ impl BacktestPipeline {
|
|||||||
|
|
||||||
if !params.skip_compile {
|
if !params.skip_compile {
|
||||||
self.log_progress(&progress_log, "COMPILE").await;
|
self.log_progress(&progress_log, "COMPILE").await;
|
||||||
self.compile_ea(¶ms.expert).await?;
|
self.compile_ea(¶ms.expert, params.timeout).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !params.skip_clean {
|
if !params.skip_clean {
|
||||||
@@ -90,6 +90,13 @@ impl BacktestPipeline {
|
|||||||
&report_dir.to_string_lossy(),
|
&report_dir.to_string_lossy(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Handle case where EA didn't trade - no deals generated
|
||||||
|
if extraction.deals.is_empty() {
|
||||||
|
tracing::warn!("Backtest completed but no deals were generated - EA did not trade during this period");
|
||||||
|
let warning_path = report_dir.join("NO_TRADES_WARNING.txt");
|
||||||
|
let _ = fs::write(&warning_path, "Warning: No deals were generated during this backtest.\nThe EA did not execute any trades during the specified date range.\n");
|
||||||
|
}
|
||||||
|
|
||||||
// Move equity chart images to OS temp dir, then delete the HTML report.
|
// Move equity chart images to OS temp dir, then delete the HTML report.
|
||||||
let charts_dir = self.relocate_charts(&report_path, &report_id).await;
|
let charts_dir = self.relocate_charts(&report_path, &report_id).await;
|
||||||
let _ = fs::remove_file(&report_path);
|
let _ = fs::remove_file(&report_path);
|
||||||
@@ -108,7 +115,7 @@ impl BacktestPipeline {
|
|||||||
self.log_progress(&progress_log, "DONE").await;
|
self.log_progress(&progress_log, "DONE").await;
|
||||||
|
|
||||||
let duration = (chrono::Utc::now() - start_time).num_seconds();
|
let duration = (chrono::Utc::now() - start_time).num_seconds();
|
||||||
self.save_metadata(¶ms, &report_dir, duration).await?;
|
self.save_metadata(¶ms, &report_dir, duration, extraction.deals.is_empty()).await?;
|
||||||
|
|
||||||
// Register in the SQLite report registry.
|
// Register in the SQLite report registry.
|
||||||
self.register_in_db(
|
self.register_in_db(
|
||||||
@@ -122,14 +129,105 @@ impl BacktestPipeline {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
let message = if extraction.deals.is_empty() {
|
||||||
|
"Backtest completed successfully, but EA did not execute any trades during this period".to_string()
|
||||||
|
} else {
|
||||||
|
"Backtest completed successfully".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(PipelineResult {
|
Ok(PipelineResult {
|
||||||
success: true,
|
success: true,
|
||||||
report_dir,
|
report_dir,
|
||||||
duration_seconds: duration,
|
duration_seconds: duration,
|
||||||
message: "Backtest completed successfully".to_string(),
|
message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Launch backtest in fire-and-forget mode: compile, clean, launch MT5, return immediately.
|
||||||
|
/// Returns a BacktestJob that can be used with get_backtest_status to poll for completion.
|
||||||
|
pub async fn launch_backtest(&self, params: BacktestParams) -> Result<BacktestJob> {
|
||||||
|
let _start_time = chrono::Utc::now();
|
||||||
|
let report_id = self.generate_report_id(¶ms);
|
||||||
|
let report_dir = self.config.reports_dir().join(&report_id);
|
||||||
|
|
||||||
|
fs::create_dir_all(&report_dir)?;
|
||||||
|
|
||||||
|
let progress_log = report_dir.join("progress.log");
|
||||||
|
self.log_progress(&progress_log, "START").await;
|
||||||
|
|
||||||
|
if !params.skip_compile {
|
||||||
|
self.log_progress(&progress_log, "COMPILE").await;
|
||||||
|
self.compile_ea(¶ms.expert, params.timeout).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !params.skip_clean {
|
||||||
|
self.log_progress(&progress_log, "CLEAN").await;
|
||||||
|
self.clean_cache(¶ms.expert).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.log_progress(&progress_log, "BACKTEST").await;
|
||||||
|
|
||||||
|
// Get MT5 paths
|
||||||
|
let mt5_dir = self.config.mt5_dir()
|
||||||
|
.ok_or_else(|| anyhow!("MT5 directory not configured"))?;
|
||||||
|
let wine_exe = self.config.wine_executable.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("wine_executable not configured"))?;
|
||||||
|
let wine_prefix = mt5_dir
|
||||||
|
.parent()
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.map(|p| p.to_path_buf())
|
||||||
|
.ok_or_else(|| anyhow!("Could not determine Wine prefix from terminal_dir"))?;
|
||||||
|
let reports_dir = mt5_dir.join("reports");
|
||||||
|
fs::create_dir_all(&reports_dir)?;
|
||||||
|
|
||||||
|
// Write config files
|
||||||
|
let ini_content = self.build_backtest_ini(¶ms, &report_id)?;
|
||||||
|
let config_host = wine_prefix.join("drive_c").join("backtest_config.ini");
|
||||||
|
fs::write(&config_host, ini_content.as_bytes())?;
|
||||||
|
self.update_terminal_ini(¶ms, &report_id)?;
|
||||||
|
|
||||||
|
// Kill any running MT5
|
||||||
|
self.kill_mt5().await?;
|
||||||
|
|
||||||
|
// Launch MT5 (fire and forget)
|
||||||
|
let mut cmd = self.build_wine_launch(wine_exe, &wine_prefix)?;
|
||||||
|
let child = cmd.stdin(std::process::Stdio::null())
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let pid = child.id();
|
||||||
|
tracing::info!("MT5 launched with PID {:?} for backtest {}", pid, report_id);
|
||||||
|
|
||||||
|
// Create and save the job tracking file
|
||||||
|
let expected_report = reports_dir.join(format!("{}.htm", report_id));
|
||||||
|
let job = BacktestJob::new(
|
||||||
|
report_id.clone(),
|
||||||
|
report_dir.to_string_lossy().to_string(),
|
||||||
|
params.expert.clone(),
|
||||||
|
params.symbol.clone(),
|
||||||
|
params.timeframe.clone(),
|
||||||
|
expected_report.to_string_lossy().to_string(),
|
||||||
|
params.timeout,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save job info for polling
|
||||||
|
let job_path = report_dir.join("job.json");
|
||||||
|
fs::write(&job_path, serde_json::to_string_pretty(&job)?)?;
|
||||||
|
|
||||||
|
// Save initial metadata
|
||||||
|
self.save_metadata(¶ms, &report_dir, 0, false).await?;
|
||||||
|
|
||||||
|
// Register in DB as "running"
|
||||||
|
let db = ReportDb::new(&Config::db_path());
|
||||||
|
if let Err(e) = db.init() {
|
||||||
|
tracing::warn!("Failed to init report DB: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(job)
|
||||||
|
}
|
||||||
|
|
||||||
/// Move equity chart images (*.png, *.gif) from MT5's reports dir to OS temp,
|
/// Move equity chart images (*.png, *.gif) from MT5's reports dir to OS temp,
|
||||||
/// returning the temp path if any images were found.
|
/// returning the temp path if any images were found.
|
||||||
async fn relocate_charts(&self, html_path: &Path, report_id: &str) -> Option<PathBuf> {
|
async fn relocate_charts(&self, html_path: &Path, report_id: &str) -> Option<PathBuf> {
|
||||||
@@ -232,7 +330,7 @@ impl BacktestPipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn compile_ea(&self, expert: &str) -> Result<()> {
|
async fn compile_ea(&self, expert: &str, timeout_secs: u64) -> Result<()> {
|
||||||
let mut search_paths = vec![
|
let mut search_paths = vec![
|
||||||
PathBuf::from(&self.config.get("project_dir")).join("src/experts").join(format!("{}.mq5", expert)),
|
PathBuf::from(&self.config.get("project_dir")).join("src/experts").join(format!("{}.mq5", expert)),
|
||||||
PathBuf::from(&self.config.get("project_dir")).join("src").join(format!("{}.mq5", expert)),
|
PathBuf::from(&self.config.get("project_dir")).join("src").join(format!("{}.mq5", expert)),
|
||||||
@@ -252,7 +350,8 @@ impl BacktestPipeline {
|
|||||||
.find(|p| p.exists())
|
.find(|p| p.exists())
|
||||||
.ok_or_else(|| anyhow!("Cannot find {}.mq5 — searched project_dir and MT5 Experts dir", expert))?;
|
.ok_or_else(|| anyhow!("Cannot find {}.mq5 — searched project_dir and MT5 Experts dir", expert))?;
|
||||||
|
|
||||||
let result = self.compiler.compile(&source_path.to_string_lossy())?;
|
let timeout = std::time::Duration::from_secs(timeout_secs.min(300)); // Max 5 min for compile
|
||||||
|
let result = self.compiler.compile_with_timeout(&source_path.to_string_lossy(), timeout).await?;
|
||||||
|
|
||||||
if !result.success {
|
if !result.success {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
@@ -761,7 +860,7 @@ impl BacktestPipeline {
|
|||||||
let _ = fs::write(log_path, line);
|
let _ = fs::write(log_path, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_metadata(&self, params: &BacktestParams, report_dir: &Path, duration: i64) -> Result<()> {
|
async fn save_metadata(&self, params: &BacktestParams, report_dir: &Path, duration: i64, no_trades: bool) -> Result<()> {
|
||||||
let metadata = PipelineMetadata {
|
let metadata = PipelineMetadata {
|
||||||
expert: params.expert.clone(),
|
expert: params.expert.clone(),
|
||||||
symbol: params.symbol.clone(),
|
symbol: params.symbol.clone(),
|
||||||
@@ -781,6 +880,7 @@ impl BacktestPipeline {
|
|||||||
deals_csv: report_dir.join("deals.csv").to_string_lossy().to_string(),
|
deals_csv: report_dir.join("deals.csv").to_string_lossy().to_string(),
|
||||||
deals_json: report_dir.join("deals.json").to_string_lossy().to_string(),
|
deals_json: report_dir.join("deals.json").to_string_lossy().to_string(),
|
||||||
},
|
},
|
||||||
|
no_trades,
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&metadata)?;
|
let json = serde_json::to_string_pretty(&metadata)?;
|
||||||
|
|||||||
@@ -3,7 +3,85 @@ use serde_json::{json, Value};
|
|||||||
pub fn tool_run_backtest() -> Value {
|
pub fn tool_run_backtest() -> Value {
|
||||||
json!({
|
json!({
|
||||||
"name": "run_backtest",
|
"name": "run_backtest",
|
||||||
"description": "Run a complete MT5 backtest pipeline: compile → clean cache → backtest → extract → analyze",
|
"description": "Full backtest pipeline: compile EA → clean cache → run backtest → extract results → analyze. Use this when you have modified the EA source code.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["expert"],
|
||||||
|
"properties": {
|
||||||
|
"expert": { "type": "string", "description": "EA name without path or extension" },
|
||||||
|
"symbol": { "type": "string", "description": "Trading symbol (default: from config or first available)" },
|
||||||
|
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD (default: past complete month)" },
|
||||||
|
"to_date": { "type": "string", "description": "End date YYYY.MM.DD (default: past complete month)" },
|
||||||
|
"timeframe": { "type": "string", "enum": ["M1", "M5", "M15", "M30", "H1", "H4", "D1"], "description": "Chart timeframe (default: M5)" },
|
||||||
|
"deposit": { "type": "integer", "description": "Initial deposit (default: 10000)" },
|
||||||
|
"model": { "type": "integer", "enum": [0, 1, 2], "description": "Tick model: 0=Every tick, 1=OHLC, 2=Open prices" },
|
||||||
|
"set_file": { "type": "string", "description": "Path to .set parameter file for EA inputs" },
|
||||||
|
"skip_compile": { "type": "boolean", "description": "Skip compilation (use existing .ex5)" },
|
||||||
|
"skip_clean": { "type": "boolean", "description": "Skip cache cleaning" },
|
||||||
|
"skip_analyze": { "type": "boolean", "description": "Skip analysis phase" },
|
||||||
|
"deep": { "type": "boolean", "description": "Run deep analysis with extra metrics" },
|
||||||
|
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest completes" },
|
||||||
|
"kill_existing": { "type": "boolean", "description": "Kill any running MT5 instance first" },
|
||||||
|
"timeout": { "type": "integer", "description": "Max wait time in seconds (default: 900)" },
|
||||||
|
"gui": { "type": "boolean", "description": "Enable MT5 visualization window" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tool_run_backtest_quick() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "run_backtest_quick",
|
||||||
|
"description": "Quick backtest using pre-compiled EA: clean cache → run backtest → extract → analyze. Skips compilation. Use when EA code hasn't changed.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["expert"],
|
||||||
|
"properties": {
|
||||||
|
"expert": { "type": "string", "description": "EA name without path or extension (must have .ex5 compiled)" },
|
||||||
|
"symbol": { "type": "string", "description": "Trading symbol (default: from config or first available)" },
|
||||||
|
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD (default: past complete month)" },
|
||||||
|
"to_date": { "type": "string", "description": "End date YYYY.MM.DD (default: past complete month)" },
|
||||||
|
"timeframe": { "type": "string", "enum": ["M1", "M5", "M15", "M30", "H1", "H4", "D1"], "description": "Chart timeframe (default: M5)" },
|
||||||
|
"deposit": { "type": "integer", "description": "Initial deposit (default: 10000)" },
|
||||||
|
"model": { "type": "integer", "enum": [0, 1, 2], "description": "Tick model" },
|
||||||
|
"set_file": { "type": "string", "description": "Path to .set parameter file for EA inputs" },
|
||||||
|
"deep": { "type": "boolean", "description": "Run deep analysis" },
|
||||||
|
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest" },
|
||||||
|
"timeout": { "type": "integer", "description": "Max wait time in seconds (default: 900)" },
|
||||||
|
"gui": { "type": "boolean", "description": "Enable MT5 visualization" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tool_run_backtest_only() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "run_backtest_only",
|
||||||
|
"description": "Backtest only: just run backtest and extract results. No compile, no analysis. Fastest option when you just need raw trade data.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["expert"],
|
||||||
|
"properties": {
|
||||||
|
"expert": { "type": "string", "description": "EA name without path or extension (must have .ex5 compiled)" },
|
||||||
|
"symbol": { "type": "string", "description": "Trading symbol (default: from config)" },
|
||||||
|
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD" },
|
||||||
|
"to_date": { "type": "string", "description": "End date YYYY.MM.DD" },
|
||||||
|
"timeframe": { "type": "string", "enum": ["M1", "M5", "M15", "M30", "H1", "H4", "D1"], "description": "Chart timeframe (default: M5)" },
|
||||||
|
"deposit": { "type": "integer", "description": "Initial deposit (default: 10000)" },
|
||||||
|
"model": { "type": "integer", "enum": [0, 1, 2], "description": "Tick model" },
|
||||||
|
"set_file": { "type": "string", "description": "Path to .set parameter file" },
|
||||||
|
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest" },
|
||||||
|
"timeout": { "type": "integer", "description": "Max wait time (default: 900)" },
|
||||||
|
"gui": { "type": "boolean", "description": "Enable MT5 visualization" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tool_launch_backtest() -> Value {
|
||||||
|
json!({
|
||||||
|
"name": "launch_backtest",
|
||||||
|
"description": "Launch MT5 backtest in fire-and-forget mode: compile → clean cache → launch MT5 backtest, then return immediately. Use get_backtest_status to poll for completion.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["expert"],
|
"required": ["expert"],
|
||||||
@@ -18,12 +96,8 @@ pub fn tool_run_backtest() -> Value {
|
|||||||
"set_file": { "type": "string", "description": "Path to .set parameter file" },
|
"set_file": { "type": "string", "description": "Path to .set parameter file" },
|
||||||
"skip_compile": { "type": "boolean" },
|
"skip_compile": { "type": "boolean" },
|
||||||
"skip_clean": { "type": "boolean" },
|
"skip_clean": { "type": "boolean" },
|
||||||
"skip_analyze": { "type": "boolean" },
|
"timeout": { "type": "integer", "description": "Max time in seconds to wait for backtest (default: 900)" },
|
||||||
"deep": { "type": "boolean", "description": "Run deep analysis" },
|
"gui": { "type": "boolean", "description": "Enable visualization during backtest" }
|
||||||
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest" },
|
|
||||||
"kill_existing": { "type": "boolean" },
|
|
||||||
"timeout": { "type": "integer" },
|
|
||||||
"gui": { "type": "boolean" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ pub mod utility;
|
|||||||
|
|
||||||
pub fn get_tools_list() -> Value {
|
pub fn get_tools_list() -> Value {
|
||||||
let tools = vec![
|
let tools = vec![
|
||||||
// Backtest
|
// Backtest - Granular options
|
||||||
backtest::tool_run_backtest(),
|
backtest::tool_run_backtest(), // Full pipeline: compile + clean + backtest + extract + analyze
|
||||||
backtest::tool_get_backtest_status(),
|
backtest::tool_run_backtest_quick(), // Quick: skip compile, do clean + backtest + extract + analyze
|
||||||
|
backtest::tool_run_backtest_only(), // Minimal: skip compile, do clean + backtest + extract only
|
||||||
|
backtest::tool_launch_backtest(), // Fire-and-forget: compile + clean + launch MT5
|
||||||
|
backtest::tool_get_backtest_status(), // Poll for completion
|
||||||
backtest::tool_cache_status(),
|
backtest::tool_cache_status(),
|
||||||
backtest::tool_clean_cache(),
|
backtest::tool_clean_cache(),
|
||||||
// Optimization
|
// Optimization
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ use serde_json::{json, Value};
|
|||||||
pub fn tool_run_optimization() -> Value {
|
pub fn tool_run_optimization() -> Value {
|
||||||
json!({
|
json!({
|
||||||
"name": "run_optimization",
|
"name": "run_optimization",
|
||||||
"description": "Launch MT5 genetic parameter optimization",
|
"description": "Launch MT5 genetic parameter optimization in fire-and-forget mode. Returns immediately with job_id. Use get_optimization_status to poll for completion. Optimization typically runs for 2-6 hours.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["expert", "set_file", "from_date", "to_date"],
|
"required": ["expert", "set_file", "from_date", "to_date"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"expert": { "type": "string" },
|
"expert": { "type": "string", "description": "EA name without path or extension" },
|
||||||
"set_file": { "type": "string" },
|
"set_file": { "type": "string", "description": "Path to .set file with parameter ranges for optimization" },
|
||||||
"symbol": { "type": "string" },
|
"symbol": { "type": "string", "description": "Trading symbol (default: XAUUSD)" },
|
||||||
"from_date": { "type": "string" },
|
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD" },
|
||||||
"to_date": { "type": "string" },
|
"to_date": { "type": "string", "description": "End date YYYY.MM.DD" },
|
||||||
"deposit": { "type": "integer" }
|
"deposit": { "type": "integer", "description": "Initial deposit (default: 10000)" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -22,12 +22,12 @@ pub fn tool_run_optimization() -> Value {
|
|||||||
pub fn tool_get_optimization_status() -> Value {
|
pub fn tool_get_optimization_status() -> Value {
|
||||||
json!({
|
json!({
|
||||||
"name": "get_optimization_status",
|
"name": "get_optimization_status",
|
||||||
"description": "Check progress of a running optimization job",
|
"description": "Check progress of a running optimization job. Poll periodically until status shows 'completed'.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["job_id"],
|
"required": ["job_id"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"job_id": { "type": "string" }
|
"job_id": { "type": "string", "description": "Job ID returned by run_optimization" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -36,14 +36,14 @@ pub fn tool_get_optimization_status() -> Value {
|
|||||||
pub fn tool_get_optimization_results() -> Value {
|
pub fn tool_get_optimization_results() -> Value {
|
||||||
json!({
|
json!({
|
||||||
"name": "get_optimization_results",
|
"name": "get_optimization_results",
|
||||||
"description": "Parse completed MT5 optimization results",
|
"description": "Parse completed MT5 optimization results and find best parameter combinations",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"job_id": { "type": "string" },
|
"job_id": { "type": "string", "description": "Job ID to parse results for" },
|
||||||
"report_file": { "type": "string" },
|
"report_file": { "type": "string", "description": "Direct path to optimization report XML file" },
|
||||||
"dd_threshold": { "type": "number" },
|
"dd_threshold": { "type": "number", "description": "Max drawdown percentage filter" },
|
||||||
"top_n": { "type": "integer" }
|
"top_n": { "type": "integer", "description": "Number of top passes to return (default: 30)" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -52,7 +52,7 @@ pub fn tool_get_optimization_results() -> Value {
|
|||||||
pub fn tool_list_jobs() -> Value {
|
pub fn tool_list_jobs() -> Value {
|
||||||
json!({
|
json!({
|
||||||
"name": "list_jobs",
|
"name": "list_jobs",
|
||||||
"description": "List running and completed optimization jobs",
|
"description": "List all running and completed optimization jobs with their status",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
|
|||||||
+217
-10
@@ -2,7 +2,9 @@ use anyhow::Result;
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
use crate::models::Config;
|
use crate::models::Config;
|
||||||
|
use crate::models::report::BacktestJob;
|
||||||
use crate::pipeline::backtest::{BacktestParams, BacktestPipeline};
|
use crate::pipeline::backtest::{BacktestParams, BacktestPipeline};
|
||||||
|
|
||||||
/// Pre-flight check result for backtest readiness
|
/// Pre-flight check result for backtest readiness
|
||||||
@@ -190,38 +192,243 @@ pub async fn handle_run_backtest(config: &Config, args: &Value) -> Result<Value>
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_run_backtest_quick(config: &Config, args: &Value) -> Result<Value> {
|
||||||
|
// Quick backtest: skip compile, do clean → backtest → extract → analyze
|
||||||
|
let mut args = args.clone();
|
||||||
|
if let Some(obj) = args.as_object_mut() {
|
||||||
|
obj.insert("skip_compile".to_string(), json!(true));
|
||||||
|
// keep skip_analyze as false (default) to run analysis
|
||||||
|
}
|
||||||
|
handle_run_backtest(config, &args).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_run_backtest_only(config: &Config, args: &Value) -> Result<Value> {
|
||||||
|
// Backtest only: skip compile, skip analyze - just backtest and extract
|
||||||
|
let mut args = args.clone();
|
||||||
|
if let Some(obj) = args.as_object_mut() {
|
||||||
|
obj.insert("skip_compile".to_string(), json!(true));
|
||||||
|
obj.insert("skip_analyze".to_string(), json!(true));
|
||||||
|
}
|
||||||
|
handle_run_backtest(config, &args).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_launch_backtest(config: &Config, args: &Value) -> Result<Value> {
|
||||||
|
let expert = args.get("expert")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("expert is required"))?;
|
||||||
|
|
||||||
|
// Run pre-flight check
|
||||||
|
let preflight = BacktestPreflight::check(config, expert);
|
||||||
|
|
||||||
|
// Check account session
|
||||||
|
if preflight.account.is_none() {
|
||||||
|
return Ok(json!({
|
||||||
|
"content": [{ "type": "text", "text": json!({
|
||||||
|
"error": "No active MT5 account session detected.",
|
||||||
|
"hint": "Open MT5 and login to your trading account before running backtests."
|
||||||
|
}).to_string() }],
|
||||||
|
"isError": true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get symbol
|
||||||
|
let requested_symbol = args.get("symbol")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let symbol = if requested_symbol.is_empty() {
|
||||||
|
config.backtest_symbol.clone()
|
||||||
|
.or_else(|| preflight.available_symbols.first().cloned())
|
||||||
|
.unwrap_or_else(|| "EURUSD".to_string())
|
||||||
|
} else {
|
||||||
|
requested_symbol.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// EA existence check
|
||||||
|
if !preflight.ea_exists {
|
||||||
|
return Ok(json!({
|
||||||
|
"content": [{ "type": "text", "text": json!({
|
||||||
|
"error": format!("EA '{}' not found in Experts directory.", expert),
|
||||||
|
"hint": "Use search_experts or list_experts to find available EAs."
|
||||||
|
}).to_string() }],
|
||||||
|
"isError": true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date defaulting
|
||||||
|
let (from_date, to_date) = {
|
||||||
|
let f = args.get("from_date").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let t = args.get("to_date").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
if f.is_empty() || t.is_empty() {
|
||||||
|
super::past_complete_month()
|
||||||
|
} else {
|
||||||
|
(f.to_string(), t.to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let params = BacktestParams {
|
||||||
|
expert: expert.to_string(),
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
from_date: from_date.to_string(),
|
||||||
|
to_date: to_date.to_string(),
|
||||||
|
timeframe: args.get("timeframe").and_then(|v| v.as_str()).unwrap_or("M5").to_string(),
|
||||||
|
deposit: args.get("deposit").and_then(|v| v.as_u64()).unwrap_or(10000) as u32,
|
||||||
|
model: args.get("model").and_then(|v| v.as_u64()).unwrap_or(0) as u8,
|
||||||
|
leverage: args.get("leverage").and_then(|v| v.as_u64()).unwrap_or(500) as u32,
|
||||||
|
set_file: args.get("set_file").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||||
|
skip_compile: args.get("skip_compile").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||||
|
skip_clean: args.get("skip_clean").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||||
|
skip_analyze: true, // Not needed for launch mode
|
||||||
|
deep_analyze: false,
|
||||||
|
shutdown: false, // Don't shutdown so we can poll
|
||||||
|
kill_existing: false,
|
||||||
|
timeout: args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(900),
|
||||||
|
gui: args.get("gui").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pipeline = BacktestPipeline::new(config.clone());
|
||||||
|
let job = pipeline.launch_backtest(params).await?;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"content": [{ "type": "text", "text": json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "Backtest launched successfully. Use get_backtest_status to poll for completion.",
|
||||||
|
"report_id": job.report_id,
|
||||||
|
"report_dir": job.report_dir,
|
||||||
|
"expert": job.expert,
|
||||||
|
"symbol": job.symbol,
|
||||||
|
"timeframe": job.timeframe,
|
||||||
|
"launched_at": job.launched_at,
|
||||||
|
"timeout_seconds": job.timeout_seconds,
|
||||||
|
"poll_hint": "Call get_backtest_status with report_dir to check progress"
|
||||||
|
}).to_string() }],
|
||||||
|
"isError": false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_get_backtest_status(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_get_backtest_status(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = args.get("report_dir")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("latest");
|
.unwrap_or("latest");
|
||||||
|
|
||||||
let progress_file = Path::new(report_dir).join("progress.log");
|
let report_path = Path::new(report_dir);
|
||||||
|
let progress_file = report_path.join("progress.log");
|
||||||
|
let job_file = report_path.join("job.json");
|
||||||
|
|
||||||
let status = if progress_file.exists() {
|
// Load job info if available
|
||||||
|
let job: Option<BacktestJob> = if job_file.exists() {
|
||||||
|
fs::read_to_string(&job_file)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check progress log for stage
|
||||||
|
let (stage, progress_lines) = if progress_file.exists() {
|
||||||
if let Ok(content) = fs::read_to_string(&progress_file) {
|
if let Ok(content) = fs::read_to_string(&progress_file) {
|
||||||
let last_line = content.lines().last().unwrap_or("");
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
if last_line.contains("DONE") {
|
let last_stage = lines.last()
|
||||||
"completed"
|
.and_then(|l| l.split_whitespace().next())
|
||||||
} else {
|
.unwrap_or("UNKNOWN");
|
||||||
"running"
|
(last_stage.to_string(), lines.len())
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
"unknown"
|
("UNKNOWN".to_string(), 0)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
("NOT_STARTED".to_string(), 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if MT5 is running
|
||||||
|
let mt5_running = is_mt5_running();
|
||||||
|
|
||||||
|
// Check if report file exists
|
||||||
|
let report_found = job.as_ref()
|
||||||
|
.map(|j| Path::new(&j.expected_report_path).exists())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// Check for completed artifacts
|
||||||
|
let metrics_exists = report_path.join("metrics.json").exists();
|
||||||
|
let deals_exists = report_path.join("deals.csv").exists();
|
||||||
|
let is_complete = stage == "DONE" || (report_found && metrics_exists);
|
||||||
|
|
||||||
|
// Calculate elapsed time if job exists
|
||||||
|
let elapsed_seconds = job.as_ref()
|
||||||
|
.and_then(|j| {
|
||||||
|
chrono::DateTime::parse_from_rfc3339(&j.launched_at)
|
||||||
|
.ok()
|
||||||
|
.map(|t| (chrono::Utc::now() - t.with_timezone(&chrono::Utc)).num_seconds())
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Determine status message
|
||||||
|
let status_msg = if is_complete {
|
||||||
|
"completed"
|
||||||
|
} else if stage == "BACKTEST" && mt5_running {
|
||||||
|
"running"
|
||||||
|
} else if stage == "BACKTEST" && !mt5_running && !report_found {
|
||||||
|
"failed"
|
||||||
|
} else if progress_lines > 0 {
|
||||||
|
"in_progress"
|
||||||
} else {
|
} else {
|
||||||
"not_started"
|
"not_started"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let message = if is_complete {
|
||||||
|
"Backtest completed successfully"
|
||||||
|
} else if stage == "BACKTEST" && mt5_running {
|
||||||
|
"MT5 is running the backtest"
|
||||||
|
} else if stage == "BACKTEST" && !mt5_running {
|
||||||
|
"MT5 process exited but report not found - backtest may have failed"
|
||||||
|
} else {
|
||||||
|
&format!("Backtest is at stage: {}", stage)
|
||||||
|
};
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"content": [{ "type": "text", "text": json!({
|
"content": [{ "type": "text", "text": json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"report_dir": report_dir,
|
"report_dir": report_dir,
|
||||||
"status": status
|
"status": status_msg,
|
||||||
|
"stage": stage,
|
||||||
|
"is_complete": is_complete,
|
||||||
|
"mt5_running": mt5_running,
|
||||||
|
"report_found": report_found,
|
||||||
|
"metrics_extracted": metrics_exists,
|
||||||
|
"deals_extracted": deals_exists,
|
||||||
|
"elapsed_seconds": elapsed_seconds,
|
||||||
|
"message": message,
|
||||||
|
"job": job.map(|j| {
|
||||||
|
json!({
|
||||||
|
"report_id": j.report_id,
|
||||||
|
"expert": j.expert,
|
||||||
|
"symbol": j.symbol,
|
||||||
|
"timeframe": j.timeframe,
|
||||||
|
"launched_at": j.launched_at,
|
||||||
|
"timeout_seconds": j.timeout_seconds
|
||||||
|
})
|
||||||
|
})
|
||||||
}).to_string() }],
|
}).to_string() }],
|
||||||
"isError": false
|
"isError": false
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if MT5 is currently running
|
||||||
|
fn is_mt5_running() -> bool {
|
||||||
|
let patterns = if cfg!(target_os = "macos") {
|
||||||
|
vec!["MetaTrader 5\\.app", "terminal64\\.exe"]
|
||||||
|
} else {
|
||||||
|
vec!["terminal64\\.exe", "metatrader"]
|
||||||
|
};
|
||||||
|
|
||||||
|
patterns.iter().any(|pat| {
|
||||||
|
Command::new("pgrep")
|
||||||
|
.args(["-f", pat])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_cache_status(config: &Config) -> Result<Value> {
|
pub async fn handle_cache_status(config: &Config) -> Result<Value> {
|
||||||
let cache_dir = config.tester_cache_dir.as_ref()
|
let cache_dir = config.tester_cache_dir.as_ref()
|
||||||
.map(|s| Path::new(s))
|
.map(|s| Path::new(s))
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result<Value> {
|
|||||||
let compiler = MqlCompiler::new(config.clone());
|
let compiler = MqlCompiler::new(config.clone());
|
||||||
let expert_path = resolved_path.as_str();
|
let expert_path = resolved_path.as_str();
|
||||||
|
|
||||||
match compiler.compile(&expert_path) {
|
match compiler.compile(&expert_path).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"content": [{ "type": "text", "text": json!({
|
"content": [{ "type": "text", "text": json!({
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ impl ToolHandler {
|
|||||||
"copy_indicator_to_project" => experts::handle_copy_indicator_to_project(&self.config, args).await,
|
"copy_indicator_to_project" => experts::handle_copy_indicator_to_project(&self.config, args).await,
|
||||||
"copy_script_to_project" => experts::handle_copy_script_to_project(&self.config, args).await,
|
"copy_script_to_project" => experts::handle_copy_script_to_project(&self.config, args).await,
|
||||||
|
|
||||||
// Backtest handlers
|
// Backtest handlers - Granular pipeline options
|
||||||
"run_backtest" => backtest::handle_run_backtest(&self.config, args).await,
|
"run_backtest" => backtest::handle_run_backtest(&self.config, args).await, // Full: compile + clean + backtest + extract + analyze
|
||||||
|
"run_backtest_quick" => backtest::handle_run_backtest_quick(&self.config, args).await, // Quick: skip compile, do backtest + extract + analyze
|
||||||
|
"run_backtest_only" => backtest::handle_run_backtest_only(&self.config, args).await, // Minimal: skip compile, do backtest + extract only
|
||||||
|
"launch_backtest" => backtest::handle_launch_backtest(&self.config, args).await, // Fire-and-forget mode
|
||||||
"get_backtest_status" => backtest::handle_get_backtest_status(&self.config, args).await,
|
"get_backtest_status" => backtest::handle_get_backtest_status(&self.config, args).await,
|
||||||
"cache_status" => backtest::handle_cache_status(&self.config).await,
|
"cache_status" => backtest::handle_cache_status(&self.config).await,
|
||||||
"clean_cache" => backtest::handle_clean_cache(&self.config, args).await,
|
"clean_cache" => backtest::handle_clean_cache(&self.config, args).await,
|
||||||
|
|||||||
Reference in New Issue
Block a user