18 Commits

Author SHA1 Message Date
Devid HW 7c9eeef1c4 release: v1.31.1
- Version bump 1.31.0 → 1.31.1
- server.json identifier URL updated (SHA256 set by CI after build)
- CHANGELOG.md updated
2026-04-22 06:15:56 +07:00
Devid HW 20a2d3d6e4 ci: add release automation scripts and workflows
- scripts/release.sh: one-command local release (bump, changelog, tag, push)
- .github/workflows/release.yml: fix SHA256 auto-commit back to master,
  dynamic mcp-publisher version detection, cargo cache, security hardening
- .claude/commands/release.md: /release slash command
- .windsurf/workflows/release.md: streamlined to use release.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:15:38 +07:00
Devid HW ec2a22ed30 ci: handle mcp-publisher registryType bug gracefully
- Don't fail workflow if MCP publish fails
- Add clear manual publish instructions
- Reference web UI as alternative
2026-04-22 05:58:09 +07:00
Devid HW 94a8c42e68 fix: update registryType to mcpbPackageType
- The registry expects mcpbPackageType not mcpb
2026-04-22 05:52:20 +07:00
Devid HW 7e97086909 ci: add checkout step to mcp-publish job
- server.json is needed for mcp-publisher to work
2026-04-22 05:44:43 +07:00
Devid HW 7a8a8daeca ci: use GitHub OIDC for MCP authentication
- Replace non-existent --token-stdin with github-oidc method
- Add id-token: write permission for OIDC
- Remove MCP_PUBLISH_TOKEN (not needed with OIDC)
2026-04-22 05:39:46 +07:00
Devid HW ba04405454 ci: fix mcp-publisher download URL
- Change from wrong repo (mcp-publisher) to correct repo (registry)
- Use proper tar.gz format with versioned release URL
- Add tar extraction step
- Update manual install instructions with correct URL
2026-04-22 05:32:55 +07:00
Devid HW e40dbf63cf ci: fix mcp-publisher download with error handling
- Add curl -fsSL to fail on HTTP errors
- Add graceful skip if download fails
- Add manual publish instructions as fallback
2026-04-22 05:28:01 +07:00
Devid HW 0a2e076ad6 ci: rename release artifacts to mcp-mt5-quant-* format
- Change mt5-quant-macos-arm64.tar.gz -> mcp-mt5-quant-macos-arm64.tar.gz
- Change mt5-quant-linux-x64.tar.gz -> mcp-mt5-quant-linux-x64.tar.gz
- Update build-mcp-package job extraction paths
2026-04-22 05:22:02 +07:00
Devid HW 8f71f1629d docs: unify all docs to use single installation path ~/.local/bin/mt5-quant
- Update WINDSURF.md: Use registry name io.github.masdevid/mt5-quant
- Update CURSOR.md: Use ~/.local/bin/mt5-quant path
- Update QUICKSTART.md: Use standard path for all clients
- Update VSCODE.md: Use standard path
- Update setup.sh: Prefer ~/.local/bin/mt5-quant over project path
- Add fallback warning in setup.sh if project path used

Single source of truth: ~/.local/bin/mt5-quant
2026-04-22 05:17:20 +07:00
Devid HW ad17f9dba6 ci: add release-info job with Claude/Windsurf/Cursor commands
- Add final job that displays copy-paste registration commands
- Shows Claude Code, Windsurf, and VS Code/Cursor commands
- Includes release URLs and success banner
2026-04-22 05:07:27 +07:00
Devid HW 33186d276a ci: add automated MCP publishing and client registration docs
- Update release workflow with MCP_PUBLISH_TOKEN for automated publishing
- Remove OIDC requirement (using classic token)
- Add MCP client registration commands for Windsurf, Claude, VS Code/Cursor
- Add verification steps for post-release checking
2026-04-22 05:04:18 +07:00
Devid HW 3927343999 ci: update release workflow for manual triggers and MCP publish
- Add workflow_dispatch with version input for manual releases
- Remove tag-only condition - now works with manual triggers
- Use github.event.inputs.version || github.ref_name for flexibility
- Add MCP package artifact upload
- Add mcp-publish job with OIDC support instructions
2026-04-22 04:57:23 +07:00
Devid HW 6a5253186b ci: add release workflow for automated builds
- Build macOS and Linux binaries
- Create MCP package with SHA256
- Upload to GitHub releases
2026-04-22 04:50:16 +07:00
Devid HW 76dac96583 docs: update release workflow to use single installation path
- Use ~/.local/bin/mt5-quant as single installation location
- Remove redundant project URL references
- Simplify MCP registration for all clients (Claude, Windsurf, VS Code)
2026-04-22 04:48:56 +07:00
Devid HW 0bc410f613 feat: add Wine/MT5 debugging tools and update release workflow
- Add 9 debugging/diagnostics tools for Wine/MT5 crash investigation
- Update server.json with v1.30.0 and MCP package config
- Update README.md with 85 tools count and debugging section
- Update docs/MCP_TOOLS.md documentation
- Enhance release workflow with MCP packaging job
- Clean up mcp-package directory (now built in CI)
2026-04-22 04:41:41 +07:00
Devid HW 9be1296916 fix: shorten server.json description for MCP registry
Description must be <=100 characters for registry validation.
Previous: 96 chars -> Current: 82 chars
2026-04-20 03:04:17 +07:00
Devid HW 63c0470c6b fix: update server.json with real SHA256 and shorten description
- Description: 96 chars (was >100)
- SHA256: calculated from local MCP package build
2026-04-20 02:54:28 +07:00
24 changed files with 1485 additions and 85 deletions
+114
View File
@@ -0,0 +1,114 @@
---
description: Release a new version of mt5-quant — bump, changelog, tag, push
---
# /release — MT5-Quant Release Workflow
Automates the full release cycle: version bump → changelog → git tag → GitHub push → CI triggers build + MCP publish.
## Usage
```
/release [patch|minor|major|X.Y.Z]
```
Default is `patch` if no argument is given.
---
## Step 1 — Check current version and state
```bash
grep '^version' Cargo.toml | head -1
git status --short
git log --oneline -5
```
## Step 2 — Count tools (update docs if needed)
```bash
grep -r "pub fn tool_" src/tools/definitions/ | wc -l
```
If the count changed since the last release, update:
- `README.md` — "MCP Tools (N)" section header and description
- `docs/MCP_TOOLS.md` — "documents X of Y total tools" line
## Step 3 — Run the release script
```bash
bash scripts/release.sh patch
```
Replace `patch` with `minor`, `major`, or an explicit version like `1.33.0`.
The script will:
1. Bump `Cargo.toml` version
2. Update `server.json` version + download URL (SHA256 = TBD, set by CI)
3. Generate a `CHANGELOG.md` entry from `git log` since the last tag
4. Run `cargo check` to verify the build
5. Commit all changes
6. Create an annotated git tag
7. Push branch + tag → triggers GitHub Actions
## Step 4 — Monitor GitHub Actions
```bash
gh run list --limit 5
```
Or open: https://github.com/masdevid/mt5-quant/actions
The CI pipeline will:
- Build macOS arm64 + Linux x64 binaries
- Create the GitHub Release with both artifacts
- Build the MCP installable package
- Compute the real SHA256 and commit it back to `server.json`
- Attempt to publish to the MCP Registry
## Step 5 — Install updated binary locally
```bash
cp target/release/mt5-quant ~/.local/bin/mt5-quant
```
Or rebuild from the release tag:
```bash
cargo build --release && cp target/release/mt5-quant ~/.local/bin/mt5-quant
```
## Step 6 — Verify MCP registration
```bash
claude mcp list
~/.local/bin/mt5-quant --help 2>/dev/null || echo "binary works"
```
---
## Troubleshooting
**MCP publish failed in CI?**
```bash
# Download latest mcp-publisher
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz
./mcp-publisher login github
./mcp-publisher publish
```
**Need to re-run CI without a new tag?**
```bash
gh workflow run release.yml -f version=v1.32.0
```
**Rollback a bad release?**
```bash
git tag -d v1.32.0
git push origin :refs/tags/v1.32.0
# Then delete the GitHub Release via web UI or:
gh release delete v1.32.0
```
+382
View File
@@ -0,0 +1,382 @@
name: Build and Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g., v1.32.0)'
required: true
type: string
jobs:
# ── Build binaries ───────────────────────────────────────────────────────────
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: macos-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: macos-cargo-
- 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: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: linux-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: linux-cargo-
- 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
# ── GitHub Release ───────────────────────────────────────────────────────────
release:
needs: [build-macos, build-linux]
runs-on: ubuntu-latest
permissions:
contents: write
env:
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
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 GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ env.RELEASE_TAG }}
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 }}
# ── MCP Package ──────────────────────────────────────────────────────────────
# Builds the installable MCP package (binary + config + docs),
# computes its SHA256, and uploads to the release.
build-mcp-package:
needs: release
runs-on: macos-latest
permissions:
contents: write
outputs:
sha256: ${{ steps.compute-sha256.outputs.sha256 }}
env:
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
steps:
- uses: actions/checkout@v4
- name: Download macOS binary
uses: actions/download-artifact@v4
with:
name: macos-binary
path: dist
- name: Build MCP package
run: |
mkdir -p mcp-package/mcp-server/bin
cd dist && tar -xzf mcp-mt5-quant-macos-arm64.tar.gz && cd ..
cp dist/mcp-mt5-quant-macos/mt5-quant mcp-package/mcp-server/bin/
cp -r config mcp-package/
cp -r docs mcp-package/
cp README.md mcp-package/
cp server.json mcp-package/
cd mcp-package && tar -czf ../mcp-mt5-quant-macos-arm64.tar.gz . && cd ..
- name: Compute SHA256
id: compute-sha256
run: |
SHA256=$(shasum -a 256 mcp-mt5-quant-macos-arm64.tar.gz | awk '{print $1}')
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
echo "MCP Package SHA256: $SHA256"
- name: Upload MCP package to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_TAG }}
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
# ── Update server.json SHA256 ────────────────────────────────────────────────
# Commits the computed SHA256 back to master so server.json always
# reflects the latest published package hash.
update-server-json:
needs: build-mcp-package
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: master
fetch-depth: 1
token: ${{ secrets.GITHUB_TOKEN }}
- name: Patch server.json
env:
PKG_SHA256: ${{ needs.build-mcp-package.outputs.sha256 }}
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
run: |
VERSION="${RELEASE_TAG#v}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
VERSION="$VERSION" PKG_SHA256="$PKG_SHA256" python3 - <<'PYEOF'
import json, os, re
sha256 = os.environ['PKG_SHA256']
version = os.environ['VERSION']
with open('server.json') as f:
data = json.load(f)
data['version'] = version
for pkg in data.get('packages', []):
pkg['version'] = version
pkg['fileSha256'] = sha256
if 'identifier' in pkg:
pkg['identifier'] = re.sub(
r'/v[0-9]+\.[0-9]+\.[0-9]+/',
f'/v{version}/',
pkg['identifier']
)
with open('server.json', 'w') as f:
json.dump(data, f, indent=2)
f.write('\n')
print(f"Patched: version={version}, sha256={sha256[:16]}...")
PYEOF
- name: Commit server.json
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add server.json
if git diff --cached --quiet; then
echo "server.json unchanged — no commit needed"
else
git commit -m "ci: update server.json SHA256 for v${VERSION} [skip ci]"
git push origin master
echo "Committed server.json update"
fi
# ── MCP Registry Publish ─────────────────────────────────────────────────────
mcp-publish:
needs: [build-mcp-package, update-server-json]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for GitHub OIDC
steps:
- uses: actions/checkout@v4
with:
ref: master # Pull master so server.json has the correct SHA256
- name: Download MCP package
uses: actions/download-artifact@v4
with:
name: mcp-package
path: .
- name: Install mcp-publisher
id: install-publisher
run: |
# Resolve latest release tag from GitHub API
LATEST_TAG=$(curl -sf \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/modelcontextprotocol/registry/releases/latest" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" \
2>/dev/null || echo "v1.0.0")
VERSION="${LATEST_TAG#v}"
URL="https://github.com/modelcontextprotocol/registry/releases/download/${LATEST_TAG}/mcp-publisher_${VERSION}_linux_amd64.tar.gz"
echo "Downloading mcp-publisher ${LATEST_TAG} from: $URL"
if curl -fsSL -o mcp-publisher.tar.gz "$URL"; then
tar -xzf mcp-publisher.tar.gz 2>/dev/null || true
BINARY=$(find . -maxdepth 3 -name "mcp-publisher" ! -path "./.git/*" | head -1)
if [[ -n "$BINARY" && -f "$BINARY" ]]; then
chmod +x "$BINARY"
sudo mv "$BINARY" /usr/local/bin/mcp-publisher
echo "installed=true" >> "$GITHUB_OUTPUT"
echo "Installed: $(mcp-publisher --version 2>/dev/null || echo 'ok')"
else
echo "installed=false" >> "$GITHUB_OUTPUT"
echo "Binary not found in archive"
fi
rm -f mcp-publisher.tar.gz
else
echo "installed=false" >> "$GITHUB_OUTPUT"
echo "Download failed: $URL"
fi
- name: Validate server.json before publish
id: validate
run: |
python3 - <<'PYEOF'
import json, sys
with open('server.json') as f:
data = json.load(f)
print(f" name: {data.get('name')}")
print(f" version: {data.get('version')}")
ok = True
for pkg in data.get('packages', []):
sha = pkg.get('fileSha256', '')
ident = pkg.get('identifier', '')
print(f" sha256: {sha}")
print(f" url: {ident}")
if sha in ('', 'TBD_CI_WILL_UPDATE', 'TBD_UPDATED_BY_CI'):
print('ERROR: fileSha256 is a placeholder — publish aborted')
ok = False
sys.exit(0 if ok else 1)
PYEOF
- name: Publish to MCP Registry
if: steps.install-publisher.outputs.installed == 'true' && steps.validate.outcome == 'success'
run: |
echo "=== Authenticating with GitHub OIDC ==="
mcp-publisher login github-oidc || {
echo "OIDC login failed"
exit 1
}
echo "=== Publishing ==="
mcp-publisher publish && echo "✓ Published to MCP Registry" || {
echo ""
echo "══════════════════════════════════════════════════════════"
echo " AUTOMATED MCP PUBLISH FAILED — manual steps:"
echo "══════════════════════════════════════════════════════════"
echo " 1. Download: https://github.com/modelcontextprotocol/registry/releases"
echo " 2. ./mcp-publisher login github"
echo " 3. ./mcp-publisher publish"
echo " Web UI: https://registry.modelcontextprotocol.io"
echo "══════════════════════════════════════════════════════════"
exit 0 # Don't fail the workflow — release succeeded
}
- name: Skip notice
if: steps.install-publisher.outputs.installed != 'true'
run: |
echo "mcp-publisher not installed — skipping automated publish"
echo "Manual: https://registry.modelcontextprotocol.io"
# ── Release Summary ──────────────────────────────────────────────────────────
release-info:
needs: [mcp-publish]
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
steps:
- name: Summary
run: |
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Release ${RELEASE_TAG} complete! "
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "GitHub Release:"
echo " https://github.com/masdevid/mt5-quant/releases/tag/${RELEASE_TAG}"
echo ""
echo "MCP Registry: io.github.masdevid/mt5-quant"
echo ""
echo "── MCP Client Registration ──────────────────────────────────"
echo ""
echo "Claude Code:"
echo " claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant"
echo ""
echo "Windsurf (~/.codeium/windsurf/mcp_config.json):"
echo ' {"mcpServers": {"io.github.masdevid/mt5-quant": {"command": "~/.local/bin/mt5-quant"}}}'
echo ""
echo "Cursor / VS Code (~/.cursor/mcp.json):"
echo ' {"mcpServers": {"mt5-quant": {"command": "~/.local/bin/mt5-quant"}}}'
echo ""
echo "─────────────────────────────────────────────────────────────"
+113
View File
@@ -0,0 +1,113 @@
---
description: Release workflow — bump version, build, tag, push, publish MCP
tags: [release, publish, workflow]
---
# MT5-Quant Release Workflow
One-command release: `bash scripts/release.sh [patch|minor|major|X.Y.Z]`
---
## 0. Pre-release checklist
- [ ] Features implemented and tested
- [ ] Docs updated (README.md, docs/MCP_TOOLS.md) with correct tool count
- [ ] `cargo check` passes
Check tool count:
// turbo
```bash
grep -r "pub fn tool_" src/tools/definitions/ | wc -l
```
---
## 1. Run the release script
// turbo
```bash
bash scripts/release.sh patch
```
Accepts: `patch` · `minor` · `major` · `1.32.0` · `v1.32.0`
The script handles:
- Version bump in `Cargo.toml`
- `server.json` version + download URL update
- `CHANGELOG.md` entry generation from git log
- `cargo check` verification
- Release commit + annotated git tag
- `git push` → triggers GitHub Actions
---
## 2. Monitor CI
// turbo
```bash
gh run list --limit 3
```
CI pipeline stages:
1. **build-macos** / **build-linux** — parallel Rust builds with cache
2. **release** — creates GitHub Release with both binaries
3. **build-mcp-package** — packages binary + config + docs, computes SHA256
4. **update-server-json** — commits real SHA256 back to `server.json` on master
5. **mcp-publish** — publishes to MCP Registry via GitHub OIDC
6. **release-info** — prints registration commands for all MCP clients
---
## 3. Install locally
// turbo
```bash
cp target/release/mt5-quant ~/.local/bin/mt5-quant
```
---
## 4. Verify
// turbo
```bash
claude mcp list
```
---
## Manual MCP publish (if CI publish fails)
// turbo
```bash
# Get latest mcp-publisher
LATEST=$(curl -sf https://api.github.com/repos/modelcontextprotocol/registry/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])")
VERSION="${LATEST#v}"
curl -fsSL -o pub.tar.gz "https://github.com/modelcontextprotocol/registry/releases/download/${LATEST}/mcp-publisher_${VERSION}_$(uname -s | tr '[:upper:]' '[:lower:]')_amd64.tar.gz"
tar -xzf pub.tar.gz && chmod +x mcp-publisher
./mcp-publisher login github
./mcp-publisher publish
```
---
## Re-trigger CI without a new tag
// turbo
```bash
gh workflow run release.yml -f version=v1.32.0
```
---
## Rollback
```bash
# Remove tag locally and remotely
git tag -d v1.32.0
git push origin :refs/tags/v1.32.0
# Delete GitHub Release
gh release delete v1.32.0 --yes
```
+6
View File
@@ -0,0 +1,6 @@
# Changelog
## [1.31.1] — 2026-04-22
- Minor improvements and bug fixes
Generated
+1 -1
View File
@@ -481,7 +481,7 @@ dependencies = [
[[package]]
name = "mt5-quant"
version = "1.30.0"
version = "1.31.1"
dependencies = [
"anyhow",
"base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "mt5-quant"
version = "1.30.0"
version = "1.31.1"
edition = "2021"
description = "MT5-Quant MCP Server - Exposes MT5 backtest and optimization tools via MCP"
authors = ["masdevid <masdevid@example.com>"]
+7 -4
View File
@@ -1,6 +1,6 @@
# 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?"
@@ -62,18 +62,22 @@ The AI runs the full pipeline: compile → clean cache → backtest → extract
| [VSCODE.md](docs/VSCODE.md) | VS Code setup |
| [ANTIGRAVITY.md](docs/ANTIGRAVITY.md) | Antigravity IDE setup |
| [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 |
| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common issues |
| [REMOTE_AGENTS.md](docs/REMOTE_AGENTS.md) | Linux optimization agents |
## MCP Tools (75)
## MCP Tools (87)
### Core workflow
| Tool | Description |
|------|-------------|
| `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) |
| `get_optimization_results` | Parse optimization results after MT5 finishes |
| `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 |
|------|-------------|
| `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 |
| `list_jobs` | All optimization jobs with compact status in one call |
+2 -2
View File
@@ -29,7 +29,7 @@ cargo build --release
3. Click **Add Custom MCP**
4. Enter:
- **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`
### Method 2: Edit mcp.json Directly
@@ -41,7 +41,7 @@ Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
"mcpServers": {
"mt5-quant": {
"type": "stdio",
"command": "/absolute/path/to/mt5-quant"
"command": "~/.local/bin/mt5-quant"
}
}
}
+119 -1
View File
@@ -2,7 +2,7 @@
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`
> - `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`
@@ -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`
Launch genetic parameter optimization as a detached background process.
+6 -4
View File
@@ -81,7 +81,7 @@ terminal_dir: "~/Library/Application Support/net.metaquotes.wine.metatrader5/dri
### Claude Code
```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:
@@ -96,8 +96,10 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"mt5-quant": {
"command": "/path/to/mt5-quant"
"io.github.masdevid/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": {
"mt5-quant": {
"command": "/path/to/mt5-quant"
"command": "~/.local/bin/mt5-quant"
}
}
}
+4 -4
View File
@@ -28,7 +28,7 @@ cargo build --release
2. Run `MCP: Add Server`
3. Choose **Workspace** or **User** scope
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
@@ -38,7 +38,7 @@ Add to `.vscode/mcp.json` in your workspace:
{
"servers": {
"mt5-quant": {
"command": "/absolute/path/to/mt5-quant"
"command": "~/.local/bin/mt5-quant"
}
}
}
@@ -52,7 +52,7 @@ cat > .vscode/mcp.json << 'EOF'
{
"servers": {
"mt5-quant": {
"command": "/path/to/mt5-quant"
"command": "~/.local/bin/mt5-quant"
}
}
}
@@ -62,7 +62,7 @@ EOF
### Method 3: VS Code CLI
```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
+8 -2
View File
@@ -27,8 +27,10 @@ Edit `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"mt5-quant": {
"command": "/Users/masdevid/jobs/mt5-quant/target/release/mt5-quant"
"io.github.masdevid/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:
```bash
# Install binary to standard location
cp target/release/mt5-quant ~/.local/bin/
# Then configure
bash scripts/setup.sh
```
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env bash
# scripts/release.sh — Automate version bump, changelog, git tag, and push
#
# Usage:
# bash scripts/release.sh patch # 1.31.0 → 1.31.1
# bash scripts/release.sh minor # 1.31.0 → 1.32.0
# bash scripts/release.sh major # 1.31.0 → 2.0.0
# bash scripts/release.sh 1.32.0 # explicit version
# bash scripts/release.sh v1.32.0 # with v prefix
set -euo pipefail
cd "$(dirname "$0")/.."
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${BLUE}$*${NC}"; }
ok() { echo -e "${GREEN}$*${NC}"; }
warn() { echo -e "${YELLOW}$*${NC}"; }
die() { echo -e "${RED}$*${NC}" >&2; exit 1; }
hr() { echo -e "${BLUE}────────────────────────────────────────${NC}"; }
# ── Prerequisites ─────────────────────────────────────────────────────────────
command -v cargo >/dev/null 2>&1 || die "cargo not found"
command -v python3 >/dev/null 2>&1 || die "python3 not found"
# ── Current version ───────────────────────────────────────────────────────────
current=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
[[ -n "$current" ]] || die "Could not parse version from Cargo.toml"
info "Current version: ${BOLD}$current${NC}"
# ── Compute new version ───────────────────────────────────────────────────────
bump="${1:-patch}"
IFS='.' read -r major minor patch_v <<< "$current"
case "$bump" in
major) new="${major+1}.0.0"; new="$((major + 1)).0.0" ;;
minor) new="${major}.$((minor + 1)).0" ;;
patch) new="${major}.${minor}.$((patch_v + 1))" ;;
v*.*.*) new="${bump#v}" ;;
*.*.*) new="$bump" ;;
*) die "Usage: $0 [patch|minor|major|X.Y.Z]" ;;
esac
# Validate semver
[[ "$new" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "Invalid version: $new"
hr
info "Releasing: ${BOLD}$current$new${NC}"
hr
# ── Confirm ───────────────────────────────────────────────────────────────────
read -rp "Proceed with release v${new}? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || die "Aborted"
# ── Check git state ───────────────────────────────────────────────────────────
info "Checking git state..."
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
die "Working tree has uncommitted changes — commit or stash first"
fi
current_branch=$(git rev-parse --abbrev-ref HEAD)
info "Branch: $current_branch"
# Check tag doesn't already exist
if git rev-parse "v${new}" >/dev/null 2>&1; then
die "Tag v${new} already exists"
fi
ok "Git state clean"
# ── 1. Bump Cargo.toml ────────────────────────────────────────────────────────
info "Bumping Cargo.toml..."
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' "s/^version = \"${current}\"/version = \"${new}\"/" Cargo.toml
else
sed -i "s/^version = \"${current}\"/version = \"${new}\"/" Cargo.toml
fi
# Update Cargo.lock
cargo metadata --no-deps --format-version 1 >/dev/null 2>&1 || true
ok "Cargo.toml → $new"
# ── 2. Update server.json ─────────────────────────────────────────────────────
info "Updating server.json..."
NEW_VERSION="$new" python3 - <<'PYEOF'
import json, os, re, sys
version = os.environ['NEW_VERSION']
with open('server.json') as f:
data = json.load(f)
data['version'] = version
for pkg in data.get('packages', []):
pkg['version'] = version
# Update download URL to point at new version tag
if 'identifier' in pkg:
pkg['identifier'] = re.sub(
r'/v[0-9]+\.[0-9]+\.[0-9]+/',
f'/v{version}/',
pkg['identifier']
)
# SHA256 is computed by CI after building — placeholder signals this
pkg['fileSha256'] = 'TBD_CI_WILL_UPDATE'
with open('server.json', 'w') as f:
json.dump(data, f, indent=2)
f.write('\n')
print(f" server.json version={version}, identifier URL updated")
PYEOF
ok "server.json → $new (SHA256 updated by CI)"
# ── 3. Generate CHANGELOG entry ───────────────────────────────────────────────
info "Generating CHANGELOG entry..."
prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
today=$(date +%Y-%m-%d)
{
echo "## [$new] — $today"
echo ""
if [[ -n "$prev_tag" ]]; then
git log "${prev_tag}..HEAD" --pretty=format:"- %s" --no-merges \
| grep -v "^- ci:" \
| grep -v "^- chore:" \
| grep -v "^$" \
|| echo "- Minor improvements and bug fixes"
else
echo "- Initial public release"
fi
echo ""
} > /tmp/release_entry.md
if [[ -f CHANGELOG.md ]]; then
# Insert after the first header line
tmp=$(mktemp)
head -1 CHANGELOG.md > "$tmp"
echo "" >> "$tmp"
cat /tmp/release_entry.md >> "$tmp"
tail -n +2 CHANGELOG.md >> "$tmp"
mv "$tmp" CHANGELOG.md
else
{
echo "# Changelog"
echo ""
cat /tmp/release_entry.md
} > CHANGELOG.md
fi
rm -f /tmp/release_entry.md
ok "CHANGELOG.md updated"
# ── 4. Verify build compiles ──────────────────────────────────────────────────
info "Verifying build (cargo check)..."
cargo check --quiet 2>&1 || die "cargo check failed — fix errors before releasing"
ok "Build check passed"
# ── 5. Commit ─────────────────────────────────────────────────────────────────
info "Creating release commit..."
git add Cargo.toml Cargo.lock server.json CHANGELOG.md
git commit -m "release: v${new}
- Version bump ${current}${new}
- server.json identifier URL updated (SHA256 set by CI after build)
- CHANGELOG.md updated"
ok "Release commit created"
# ── 6. Tag ────────────────────────────────────────────────────────────────────
info "Creating annotated tag v${new}..."
git tag -a "v${new}" -m "Release v${new}"
ok "Tagged v${new}"
# ── 7. Push ───────────────────────────────────────────────────────────────────
info "Pushing to GitHub..."
git push origin "$current_branch"
git push origin "v${new}"
ok "Pushed — GitHub Actions triggered"
# ── Done ──────────────────────────────────────────────────────────────────────
hr
echo ""
echo -e "${GREEN}${BOLD} Release v${new} kicked off!${NC}"
echo ""
echo -e " Actions: https://github.com/masdevid/mt5-quant/actions"
echo -e " Release: https://github.com/masdevid/mt5-quant/releases/tag/v${new}"
echo ""
echo -e " ${YELLOW}CI will compute the MCP package SHA256 and${NC}"
echo -e " ${YELLOW}commit it back to server.json automatically.${NC}"
echo ""
hr
+15 -7
View File
@@ -1024,14 +1024,20 @@ _register_with_platform() {
local platform="$1"
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
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"
elif [[ -f "${REPO_DIR}/mt5-quant" ]]; then
cmd_path="${REPO_DIR}/mt5-quant"
_warn "Using project build at $cmd_path"
_warn "Consider installing to standard location: cp $cmd_path ~/.local/bin/"
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
case "$platform" in
@@ -1066,8 +1072,10 @@ if os.path.exists(config_path):
if 'mcpServers' not in data:
data['mcpServers'] = {}
data['mcpServers']['mt5-quant'] = {
'command': '$cmd_path'
data['mcpServers']['io.github.masdevid/mt5-quant'] = {
'command': '$cmd_path',
'disabled': False,
'registry': 'io.github.masdevid/mt5-quant'
}
with open(config_path, 'w') as f:
+6 -6
View File
@@ -2,18 +2,18 @@
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.masdevid/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": {
"url": "https://github.com/masdevid/mt5-quant",
"source": "github"
},
"version": "1.30.0",
"version": "1.31.1",
"packages": [
{
"registryType": "mcpb",
"version": "1.30.0",
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.30.0/mcp-mt5-quant-macos-arm64.tar.gz",
"fileSha256": "PLACEHOLDER_SHA256",
"registryType": "mcpbPackageType",
"version": "1.31.1",
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.31.1/mcp-mt5-quant-macos-arm64.tar.gz",
"fileSha256": "TBD_CI_WILL_UPDATE",
"transport": {
"type": "stdio"
},
+22 -8
View File
@@ -1,7 +1,8 @@
use anyhow::{anyhow, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use tokio::time::timeout as tokio_timeout;
use crate::models::Config;
@@ -28,7 +29,11 @@ impl MqlCompiler {
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);
if !source_path.exists() {
return Err(anyhow!("Source file not found: {}", source_path.display()));
@@ -61,7 +66,7 @@ impl MqlCompiler {
let staged_mq5 = &sync.dest_mq5;
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
let log_path = staged_mq5.with_extension("log");
@@ -197,15 +202,16 @@ impl MqlCompiler {
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).
/// Shell script intermediary required on macOS to preserve DYLD_* vars past SIP.
fn run_metaeditor(
async fn run_metaeditor_with_timeout(
&self,
wine_exe: &str,
wine_prefix: &Path,
metaeditor: &Path,
source_mq5: &Path,
timeout: Duration,
) -> Result<()> {
let mt5_dir = metaeditor.parent().unwrap_or(metaeditor);
@@ -242,16 +248,24 @@ impl MqlCompiler {
use std::os::unix::fs::PermissionsExt;
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 {
Command::new(wine_exe)
let compile_future = tokio::process::Command::new(wine_exe)
.arg(metaeditor)
.arg(format!("/compile:{}", source_mq5.display()))
.arg("/log")
.env("WINEPREFIX", wine_prefix)
.env("WINEDEBUG", "-all")
.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(())
}
+47
View File
@@ -1,6 +1,9 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
// Re-export chrono for BacktestJob
use chrono;
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
@@ -31,6 +34,8 @@ pub struct PipelineMetadata {
pub report_dir: String,
pub duration_seconds: i64,
pub files: FilePaths,
#[serde(default)]
pub no_trades: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -48,6 +53,9 @@ pub struct BacktestStatus {
pub elapsed_seconds: i64,
pub is_complete: bool,
pub message: String,
pub report_dir: Option<String>,
pub mt5_running: Option<bool>,
pub report_found: Option<bool>,
}
#[allow(dead_code)]
@@ -60,4 +68,43 @@ pub enum PipelineStage {
Extract,
Analyze,
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
View File
@@ -8,7 +8,7 @@ use tokio::time::{sleep, Duration};
use crate::analytics::{DealAnalyzer, ReportExtractor};
use crate::compile::MqlCompiler;
use crate::models::config::Config;
use crate::models::report::{PipelineMetadata, FilePaths};
use crate::models::report::{PipelineMetadata, FilePaths, BacktestJob};
use crate::storage::{ReportDb, ReportEntry};
pub struct BacktestPipeline {
@@ -73,7 +73,7 @@ impl BacktestPipeline {
if !params.skip_compile {
self.log_progress(&progress_log, "COMPILE").await;
self.compile_ea(&params.expert).await?;
self.compile_ea(&params.expert, params.timeout).await?;
}
if !params.skip_clean {
@@ -90,6 +90,13 @@ impl BacktestPipeline {
&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.
let charts_dir = self.relocate_charts(&report_path, &report_id).await;
let _ = fs::remove_file(&report_path);
@@ -108,7 +115,7 @@ impl BacktestPipeline {
self.log_progress(&progress_log, "DONE").await;
let duration = (chrono::Utc::now() - start_time).num_seconds();
self.save_metadata(&params, &report_dir, duration).await?;
self.save_metadata(&params, &report_dir, duration, extraction.deals.is_empty()).await?;
// Register in the SQLite report registry.
self.register_in_db(
@@ -122,14 +129,105 @@ impl BacktestPipeline {
)
.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 {
success: true,
report_dir,
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(&params);
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(&params.expert, params.timeout).await?;
}
if !params.skip_clean {
self.log_progress(&progress_log, "CLEAN").await;
self.clean_cache(&params.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(&params, &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(&params, &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(&params, &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,
/// returning the temp path if any images were found.
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![
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)),
@@ -252,7 +350,8 @@ impl BacktestPipeline {
.find(|p| p.exists())
.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 {
return Err(anyhow!(
@@ -761,7 +860,7 @@ impl BacktestPipeline {
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 {
expert: params.expert.clone(),
symbol: params.symbol.clone(),
@@ -781,6 +880,7 @@ impl BacktestPipeline {
deals_csv: report_dir.join("deals.csv").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)?;
+81 -7
View File
@@ -3,7 +3,85 @@ use serde_json::{json, Value};
pub fn tool_run_backtest() -> Value {
json!({
"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": {
"type": "object",
"required": ["expert"],
@@ -18,12 +96,8 @@ pub fn tool_run_backtest() -> Value {
"set_file": { "type": "string", "description": "Path to .set parameter file" },
"skip_compile": { "type": "boolean" },
"skip_clean": { "type": "boolean" },
"skip_analyze": { "type": "boolean" },
"deep": { "type": "boolean", "description": "Run deep analysis" },
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest" },
"kill_existing": { "type": "boolean" },
"timeout": { "type": "integer" },
"gui": { "type": "boolean" }
"timeout": { "type": "integer", "description": "Max time in seconds to wait for backtest (default: 900)" },
"gui": { "type": "boolean", "description": "Enable visualization during backtest" }
}
}
})
+6 -3
View File
@@ -12,9 +12,12 @@ pub mod utility;
pub fn get_tools_list() -> Value {
let tools = vec![
// Backtest
backtest::tool_run_backtest(),
backtest::tool_get_backtest_status(),
// Backtest - Granular options
backtest::tool_run_backtest(), // Full pipeline: compile + clean + backtest + extract + analyze
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_clean_cache(),
// Optimization
+15 -15
View File
@@ -3,17 +3,17 @@ use serde_json::{json, Value};
pub fn tool_run_optimization() -> Value {
json!({
"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": {
"type": "object",
"required": ["expert", "set_file", "from_date", "to_date"],
"properties": {
"expert": { "type": "string" },
"set_file": { "type": "string" },
"symbol": { "type": "string" },
"from_date": { "type": "string" },
"to_date": { "type": "string" },
"deposit": { "type": "integer" }
"expert": { "type": "string", "description": "EA name without path or extension" },
"set_file": { "type": "string", "description": "Path to .set file with parameter ranges for optimization" },
"symbol": { "type": "string", "description": "Trading symbol (default: XAUUSD)" },
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD" },
"to_date": { "type": "string", "description": "End date YYYY.MM.DD" },
"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 {
json!({
"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": {
"type": "object",
"required": ["job_id"],
"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 {
json!({
"name": "get_optimization_results",
"description": "Parse completed MT5 optimization results",
"description": "Parse completed MT5 optimization results and find best parameter combinations",
"inputSchema": {
"type": "object",
"properties": {
"job_id": { "type": "string" },
"report_file": { "type": "string" },
"dd_threshold": { "type": "number" },
"top_n": { "type": "integer" }
"job_id": { "type": "string", "description": "Job ID to parse results for" },
"report_file": { "type": "string", "description": "Direct path to optimization report XML file" },
"dd_threshold": { "type": "number", "description": "Max drawdown percentage filter" },
"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 {
json!({
"name": "list_jobs",
"description": "List running and completed optimization jobs",
"description": "List all running and completed optimization jobs with their status",
"inputSchema": {
"type": "object"
}
+217 -10
View File
@@ -2,7 +2,9 @@ use anyhow::Result;
use serde_json::{json, Value};
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::models::Config;
use crate::models::report::BacktestJob;
use crate::pipeline::backtest::{BacktestParams, BacktestPipeline};
/// 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> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.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) {
let last_line = content.lines().last().unwrap_or("");
if last_line.contains("DONE") {
"completed"
} else {
"running"
}
let lines: Vec<&str> = content.lines().collect();
let last_stage = lines.last()
.and_then(|l| l.split_whitespace().next())
.unwrap_or("UNKNOWN");
(last_stage.to_string(), lines.len())
} 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 {
"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!({
"content": [{ "type": "text", "text": json!({
"success": true,
"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() }],
"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> {
let cache_dir = config.tester_cache_dir.as_ref()
.map(|s| Path::new(s))
+1 -1
View File
@@ -242,7 +242,7 @@ pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result<Value> {
let compiler = MqlCompiler::new(config.clone());
let expert_path = resolved_path.as_str();
match compiler.compile(&expert_path) {
match compiler.compile(&expert_path).await {
Ok(result) => {
Ok(json!({
"content": [{ "type": "text", "text": json!({
+5 -2
View File
@@ -42,8 +42,11 @@ impl ToolHandler {
"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,
// Backtest handlers
"run_backtest" => backtest::handle_run_backtest(&self.config, args).await,
// Backtest handlers - Granular pipeline options
"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,
"cache_status" => backtest::handle_cache_status(&self.config).await,
"clean_cache" => backtest::handle_clean_cache(&self.config, args).await,