9 Commits

Author SHA1 Message Date
Devid HW 836836596f release: v1.31.3
- Version bump 1.31.2 → 1.31.3
- server.json identifier URL updated (SHA256 set by CI after build)
- CHANGELOG.md updated
2026-04-22 07:20:09 +07:00
Devid HW f9d266d3ab docs: clean up public repo — remove IDE files, fix stale refs
- Untrack .claude/commands/ and .windsurf/workflows/ (personal IDE config)
- Fix .gitignore: scope /CLAUDE.md to root only, add .claude/, remove
  incorrect .github/workflows/ exclusion (CI needs these tracked)
- Rename docs/ANTIGRAVITY.md → docs/CLAUDE.md (Claude Desktop setup;
  "Antigravity" was an internal codename, confusing to public users)
- Fix stale repo name: masdevid/mt5-mcp → masdevid/mt5-quant in
  README.md, docs/CLAUDE.md, CURSOR.md, QUICKSTART.md, VSCODE.md, WINDSURF.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 07:20:03 +07:00
Devid HW 262936a1d5 release: v1.31.2
- Version bump 1.31.1 → 1.31.2
- server.json identifier URL updated (SHA256 set by CI after build)
- CHANGELOG.md updated
2026-04-22 07:02:18 +07:00
Devid HW 0ea1e779e0 refactor: reduce handler boilerplate in analysis and experts modules
analysis.rs: extract required_str, ok_response, err_response, prepare_analysis,
deal_to_json, is_closed_trade helpers — 15 granular handlers reduced from
~14 lines each to 4 lines each (-293 lines total).

experts.rs: extract scan_mql_dir (replaces 6 identical WalkDir loops),
BUILTIN_INDICATORS const (deduplicates list/search handlers), and
copy_mql_to_project (merges copy_indicator/copy_script into one impl,
public handlers delegate in 1 line each) (-256 lines total).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 07:01:58 +07:00
github-actions[bot] a94d75823a ci: update server.json SHA256 for v1.31.1 [skip ci] 2026-04-21 23:32:59 +00:00
Devid HW c7e1a03e1f fix: registryType mcpbPackageType → mcpb
MCP registry API error: 'unsupported registry type: mcpbPackageType'
Schema examples show valid values: npm, pypi, oci, nuget, mcpb

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:31:16 +07:00
github-actions[bot] 23c67e80c5 ci: update server.json SHA256 for v1.31.1 [skip ci] 2026-04-21 23:29:49 +00:00
Devid HW 5db3c7c822 ci: fix mcp-publisher install — resolve URL from release assets API
v1.6.0 dropped the version number from asset filenames
(mcp-publisher_linux_amd64.tar.gz, not mcp-publisher_1.6.0_linux_amd64.tar.gz).
Look up the linux/amd64 .tar.gz URL directly from the GitHub releases API
instead of constructing it, so future renames don't break the workflow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:27:06 +07:00
github-actions[bot] 5eb753584d ci: update server.json SHA256 for v1.31.1 [skip ci] 2026-04-21 23:18:49 +00:00
17 changed files with 615 additions and 1368 deletions
-114
View File
@@ -1,114 +0,0 @@
---
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
```
+38 -21
View File
@@ -265,33 +265,50 @@ jobs:
- name: Install mcp-publisher
id: install-publisher
run: |
# Resolve latest release tag from GitHub API
LATEST_TAG=$(curl -sf \
# Fetch the download URL for linux_amd64 directly from the release assets API
# (asset name format changed in v1.6.0 — look it up rather than constructing it)
URL=$(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")
| python3 -c "
import sys, json
r = json.load(sys.stdin)
tag = r['tag_name']
assets = r.get('assets', [])
match = next((a['browser_download_url'] for a in assets
if 'linux' in a['name'] and 'amd64' in a['name']
and a['name'].endswith('.tar.gz')
and 'sbom' not in a['name']
and 'sigstore' not in a['name']), None)
if match:
print(match)
else:
print(f'https://github.com/modelcontextprotocol/registry/releases/download/{tag}/mcp-publisher_linux_amd64.tar.gz', file=sys.stderr)
sys.exit(1)
" 2>/dev/null)
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')"
if [[ -z "$URL" ]]; then
echo "installed=false" >> "$GITHUB_OUTPUT"
echo "Could not resolve mcp-publisher download URL"
else
echo "Downloading: $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 "Binary not found in archive"
echo "Download failed: $URL"
fi
rm -f mcp-publisher.tar.gz
else
echo "installed=false" >> "$GITHUB_OUTPUT"
echo "Download failed: $URL"
fi
- name: Validate server.json before publish
+2 -4
View File
@@ -20,14 +20,12 @@ config/baseline.json
config/backtest_history.json
# Claude Code (personal, not for repo)
CLAUDE.md
/CLAUDE.md
.claude/
# Windsurf (local workflows, not for repo)
.windsurf/
# GitHub Actions (local workflows, not for repo)
.github/workflows/
# macOS
.DS_Store
-113
View File
@@ -1,113 +0,0 @@
---
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
```
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## [1.31.3] — 2026-04-22
- docs: clean up public repo — remove IDE files, fix stale refs
- release: v1.31.2
- refactor: reduce handler boilerplate in analysis and experts modules
- fix: registryType mcpbPackageType → mcpb
## [1.31.2] — 2026-04-22
- refactor: reduce handler boilerplate in analysis and experts modules
- fix: registryType mcpbPackageType → mcpb
## [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.31.1"
version = "1.31.3"
dependencies = [
"anyhow",
"base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "mt5-quant"
version = "1.31.1"
version = "1.31.3"
edition = "2021"
description = "MT5-Quant MCP Server - Exposes MT5 backtest and optimization tools via MCP"
authors = ["masdevid <masdevid@example.com>"]
+3 -3
View File
@@ -27,7 +27,7 @@ Claude: [compile → clean → backtest → analyze 1,847 deals]
### 1. Download & Setup
```bash
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-macos-arm64.tar.gz
curl -L -o mt5.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-macos-arm64.tar.gz
tar -xzf mt5.tar.gz
bash scripts/setup.sh
```
@@ -40,7 +40,7 @@ bash scripts/setup.sh
| **Windsurf** | Edit `~/.codeium/windsurf/mcp_config.json` | [WINDSURF.md →](docs/WINDSURF.md) |
| **Cursor** | Edit `~/.cursor/mcp.json` or use Settings → MCP | [CURSOR.md →](docs/CURSOR.md) |
| **VS Code** | Edit `.vscode/mcp.json` or run `MCP: Add Server` | [VSCODE.md →](docs/VSCODE.md) |
| **Antigravity** | Agent Panel → ... → MCP Servers → Edit configuration | [ANTIGRAVITY.md →](docs/ANTIGRAVITY.md) |
| **Claude Desktop** | Agent Panel → ... → MCP Servers → Edit configuration | [CLAUDE.md →](docs/CLAUDE.md) |
> **Note:** Use absolute paths like `/Users/name/mt5-quant/mt5-quant` or `$(pwd)/mt5-quant`, not relative paths like `./mt5-quant`.
@@ -60,7 +60,7 @@ The AI runs the full pipeline: compile → clean cache → backtest → extract
| [WINDSURF.md](docs/WINDSURF.md) | Windsurf IDE setup |
| [CURSOR.md](docs/CURSOR.md) | Cursor IDE setup |
| [VSCODE.md](docs/VSCODE.md) | VS Code setup |
| [ANTIGRAVITY.md](docs/ANTIGRAVITY.md) | Antigravity IDE setup |
| [CLAUDE.md](docs/CLAUDE.md) | Claude Desktop setup |
| [CONFIG.md](docs/CONFIG.md) | Configuration reference |
| [TOOLS.md](docs/MCP_TOOLS.md) | All 87 tools documented |
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design and internals |
-145
View File
@@ -1,145 +0,0 @@
# Antigravity MCP Integration Setup
## Quick Setup
### Option 1: Download Prebuilt Binary (Recommended)
```bash
# macOS (Apple Silicon)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-macos-arm64.tar.gz
tar -xzf mt5-quant.tar.gz
# Linux (x64)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-linux-x64.tar.gz
tar -xzf mt5-quant.tar.gz
```
### Option 2: Build from Source
```bash
cargo build --release
```
## Configure Antigravity
### Step 1: Open MCP Manager
1. Launch Antigravity
2. Look at the **right-side Agent Panel**
3. Click the **"..."** (More Options) menu at the top
4. Select **MCP Servers**
### Step 2: Access Configuration
1. Click **Manage MCP Servers**
2. Click **View raw config** or **Edit configuration**
3. This opens `mcp_config.json`
### Step 3: Add mt5-quant Configuration
Add to `mcp_config.json`:
```json
{
"mcpServers": {
"mt5-quant": {
"command": "/absolute/path/to/mt5-quant"
}
}
}
```
### Step 4: Reload and Verify
1. Save the file
2. **Restart Antigravity** (or reload the window)
3. Open the Agent chat and type: `What tools do you have access to?`
4. The agent should list MT5 tools like `verify_setup`, `run_backtest`, etc.
## Full Example Configuration
```json
{
"mcpServers": {
"mt5-quant": {
"command": "/Users/name/mt5-quant/target/release/mt5-quant"
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}"
}
}
}
}
```
## Environment Variables
Antigravity supports `${VAR_NAME}` syntax for environment variable substitution:
```json
{
"mcpServers": {
"mt5-quant": {
"command": "/Users/name/mt5-quant/target/release/mt5-quant",
"env": {
"CUSTOM_VAR": "${env:MY_VAR}"
}
}
}
}
```
This keeps secrets out of the config file.
## Verify Setup
In Antigravity chat, type:
```
Run verify_setup
```
Expected output:
```
Wine: /Applications/MetaTrader 5.app/.../wine64
MT5 dir: ~/Library/Application Support/.../MetaTrader 5
Display: gui
Arch: arch -x86_64
```
## Troubleshooting
### "Connection Refused" or "Tool not found"
1. Double-check the server path in `mcp_config.json`
2. Ensure the binary has execute permissions: `chmod +x /path/to/mt5-quant`
3. Try completely restarting Antigravity
4. Check the server is listed in MCP manager
### "Stdio Error" or JSON Parsing Error
1. Verify the JSON syntax in `mcp_config.json`
2. Use a JSON validator if needed
3. Ensure no trailing commas
### Agent Hallucinating Tool Parameters
If the agent makes up incorrect parameters:
1. Check the tool is actually available: "List your available tools"
2. Restart the agent session
3. Be explicit in your requests
## Configuration Location
| Platform | Path |
|----------|------|
| All | Via UI: Agent Panel → ... → MCP Servers → Manage → Edit configuration |
## Resources
- [Antigravity Documentation](https://docs.antigravity.dev/)
- [MCP Server Reference](https://github.com/modelcontextprotocol/servers)
- [MT5-Quant Tools Reference](./MCP_TOOLS.md)
+138
View File
@@ -0,0 +1,138 @@
# Claude Desktop MCP Integration Setup
## Quick Setup
### Option 1: Install via MCP Registry (Recommended)
Search for `mt5-quant` in Claude Desktop's MCP manager, or add directly to your config.
### Option 2: Download Prebuilt Binary
```bash
# macOS (Apple Silicon)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-macos-arm64.tar.gz
tar -xzf mt5-quant.tar.gz
# Linux (x64)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-linux-x64.tar.gz
tar -xzf mt5-quant.tar.gz
```
### Option 3: Build from Source
```bash
git clone https://github.com/masdevid/mt5-quant
cd mt5-quant
cargo build --release
```
## Configure Claude Desktop
### Step 1: Open MCP Configuration
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows).
### Step 2: Add mt5-quant
```json
{
"mcpServers": {
"mt5-quant": {
"command": "/absolute/path/to/mt5-quant"
}
}
}
```
### Step 3: Reload and Verify
1. Save the file
2. **Restart Claude Desktop**
3. In a new conversation, type: `What tools do you have access to?`
4. The agent should list MT5 tools like `verify_setup`, `run_backtest`, etc.
## Full Example Configuration
```json
{
"mcpServers": {
"mt5-quant": {
"command": "/Users/name/mt5-quant/target/release/mt5-quant"
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}"
}
}
}
}
```
## Environment Variables
Claude Desktop supports `${env:VAR_NAME}` syntax for environment variable substitution:
```json
{
"mcpServers": {
"mt5-quant": {
"command": "/Users/name/mt5-quant/target/release/mt5-quant",
"env": {
"MT5_MCP_HOME": "${env:HOME}/.config/mt5-quant"
}
}
}
}
```
## Verify Setup
In a Claude Desktop conversation, type:
```
Run verify_setup
```
Expected output:
```
Wine: /Applications/MetaTrader 5.app/.../wine64
MT5 dir: ~/Library/Application Support/.../MetaTrader 5
Display: gui
Arch: arch -x86_64
```
## Troubleshooting
### "Tool not found" or server not appearing
1. Double-check the absolute path in `claude_desktop_config.json`
2. Ensure the binary has execute permissions: `chmod +x /path/to/mt5-quant`
3. Restart Claude Desktop completely
4. Check Claude Desktop logs: `~/Library/Logs/Claude/` (macOS)
### JSON syntax errors
1. Validate the JSON at [jsonlint.com](https://jsonlint.com)
2. Ensure no trailing commas
3. Use absolute paths (not `~` or relative paths)
### Agent hallucinating tool parameters
1. Verify the tool is available: "List your available MCP tools"
2. Start a new conversation
3. Be explicit: "Use the `run_backtest` tool with expert=MyEA"
## Configuration Location
| Platform | Path |
|----------|------|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
## Resources
- [Claude Desktop MCP Documentation](https://docs.anthropic.com/en/docs/claude-code/mcp)
- [MCP Server Reference](https://github.com/modelcontextprotocol/servers)
- [MT5-Quant Tools Reference](./MCP_TOOLS.md)
+2 -2
View File
@@ -6,11 +6,11 @@
```bash
# macOS (Apple Silicon)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-macos-arm64.tar.gz
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-macos-arm64.tar.gz
tar -xzf mt5-quant.tar.gz
# Linux (x64)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-linux-x64.tar.gz
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-linux-x64.tar.gz
tar -xzf mt5-quant.tar.gz
```
+6 -6
View File
@@ -6,19 +6,19 @@
```bash
# macOS (Apple Silicon)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-macos-arm64.tar.gz
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-macos-arm64.tar.gz
tar -xzf mt5-quant.tar.gz
# Linux (x64)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-linux-x64.tar.gz
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-linux-x64.tar.gz
tar -xzf mt5-quant.tar.gz
```
### Option B: Build from Source
```bash
git clone https://github.com/masdevid/mt5-mcp
cd mt5-mcp
git clone https://github.com/masdevid/mt5-quant
cd mt5-quant
bash scripts/build-rust.sh
```
@@ -137,7 +137,7 @@ Add to `.vscode/mcp.json` in your workspace:
Or run `MCP: Add Server` from Command Palette.
### Antigravity
### Claude Desktop
1. Open Agent panel → Click "..." menu → MCP Servers
2. Click "Manage MCP Servers"
@@ -153,7 +153,7 @@ Or run `MCP: Add Server` from Command Palette.
}
}
```
5. Reload Antigravity to apply changes
5. Restart Claude Desktop to apply changes
## 5. Verify Setup
+2 -2
View File
@@ -6,11 +6,11 @@
```bash
# macOS (Apple Silicon)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-macos-arm64.tar.gz
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-macos-arm64.tar.gz
tar -xzf mt5-quant.tar.gz
# Linux (x64)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-linux-x64.tar.gz
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-linux-x64.tar.gz
tar -xzf mt5-quant.tar.gz
```
+2 -2
View File
@@ -6,11 +6,11 @@
```bash
# macOS (Apple Silicon)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-macos-arm64.tar.gz
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-macos-arm64.tar.gz
tar -xzf mt5-quant.tar.gz
# Linux (x64)
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-mcp/releases/latest/download/mt5-quant-linux-x64.tar.gz
curl -L -o mt5-quant.tar.gz https://github.com/masdevid/mt5-quant/releases/latest/download/mt5-quant-linux-x64.tar.gz
tar -xzf mt5-quant.tar.gz
```
+4 -4
View File
@@ -7,12 +7,12 @@
"url": "https://github.com/masdevid/mt5-quant",
"source": "github"
},
"version": "1.31.1",
"version": "1.31.3",
"packages": [
{
"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",
"registryType": "mcpb",
"version": "1.31.3",
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.31.3/mcp-mt5-quant-macos-arm64.tar.gz",
"fileSha256": "TBD_CI_WILL_UPDATE",
"transport": {
"type": "stdio"
+200 -492
View File
@@ -6,8 +6,30 @@ use std::path::Path;
use crate::analytics::DealAnalyzer;
use crate::models::deals::Deal;
use crate::models::metrics::Metrics;
use crate::models::Config;
// ── Internal helpers ──────────────────────────────────────────────────────────
fn required_str<'a>(args: &'a Value, key: &str) -> Result<&'a str> {
args.get(key)
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("{} is required", key))
}
fn ok_response(data: Value) -> Value {
json!({
"content": [{ "type": "text", "text": data.to_string() }],
"isError": false
})
}
fn err_response(msg: impl std::fmt::Display) -> Value {
json!({
"content": [{ "type": "text", "text": msg.to_string() }],
"isError": true
})
}
/// Helper to load deals and metrics from report directory
fn load_report_data(report_dir: &str) -> Result<(Vec<Deal>, Metrics)> {
let deals_csv = Path::new(report_dir).join("deals.csv");
let metrics_json = Path::new(report_dir).join("metrics.json");
@@ -17,10 +39,8 @@ fn load_report_data(report_dir: &str) -> Result<(Vec<Deal>, Metrics)> {
}
let deals = read_deals_from_csv(&deals_csv)?;
let metrics = if metrics_json.exists() {
let content = fs::read_to_string(&metrics_json)?;
serde_json::from_str(&content)?
serde_json::from_str(&fs::read_to_string(&metrics_json)?)?
} else {
Metrics::default()
};
@@ -28,75 +48,18 @@ fn load_report_data(report_dir: &str) -> Result<(Vec<Deal>, Metrics)> {
Ok((deals, metrics))
}
pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
fn prepare_analysis(report_dir: &str) -> Result<(Vec<Deal>, Metrics, DealAnalyzer)> {
let (deals, metrics) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
// Check if specific analytics requested
let requested: Option<HashSet<String>> = args.get("analytics")
.and_then(|v| v.as_array())
.map(|arr| arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect());
let top_losses_limit = args.get("top_losses_limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let all = requested.is_none();
let req = |name: &str| all || requested.as_ref().map(|s| s.contains(name)).unwrap_or(false);
// Build selective result
let mut result = json!({});
if req("monthly_pnl") || all {
result["monthly"] = json!(analyzer.monthly_pnl(&deals));
}
if req("drawdown_events") || all {
result["dd_events"] = json!(analyzer.reconstruct_dd_events(&deals, &metrics));
}
if req("top_losses") || all {
result["top_losses"] = json!(analyzer.top_losses(&deals, top_losses_limit));
}
if req("loss_sequences") || all {
result["loss_sequences"] = json!(analyzer.loss_sequences(&deals));
}
if req("position_pairs") || all {
result["position_pairs"] = json!(analyzer.position_pairs(&deals));
}
if req("direction_bias") || all {
result["direction_bias"] = json!(analyzer.direction_bias(&deals));
}
if req("streak_analysis") || all {
result["streak_analysis"] = json!(analyzer.streak_analysis(&deals));
}
if req("concurrent_peak") || all {
result["concurrent_peak"] = json!(analyzer.concurrent_peak(&deals));
}
let analysis_path = Path::new(report_dir).join("analysis.json");
fs::write(&analysis_path, serde_json::to_string_pretty(&result)?)?;
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"analysis_file": analysis_path.to_string_lossy(),
"analytics_run": requested.map(|s| s.iter().cloned().collect::<Vec<_>>()).unwrap_or_else(|| vec!["all".to_string()]),
"summary": result,
}).to_string() }],
"isError": false
}))
Ok((deals, metrics, DealAnalyzer::new()))
}
fn read_deals_from_csv(path: &Path) -> Result<Vec<Deal>> {
let content = fs::read_to_string(path)?;
let mut deals = Vec::new();
let mut lines = content.lines();
let _header = lines.next();
for line in lines {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 12 {
@@ -118,523 +81,268 @@ fn read_deals_from_csv(path: &Path) -> Result<Vec<Deal>> {
});
}
}
Ok(deals)
}
// ── Composite analytics ───────────────────────────────────────────────────────
pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = required_str(args, "report_dir")?;
let (deals, metrics, analyzer) = prepare_analysis(report_dir)?;
let requested: Option<HashSet<String>> = args.get("analytics")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect());
let top_losses_limit = args.get("top_losses_limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let all = requested.is_none();
let req = |name: &str| all || requested.as_ref().map(|s| s.contains(name)).unwrap_or(false);
let mut result = json!({});
if req("monthly_pnl") { result["monthly"] = json!(analyzer.monthly_pnl(&deals)); }
if req("drawdown_events") { result["dd_events"] = json!(analyzer.reconstruct_dd_events(&deals, &metrics)); }
if req("top_losses") { result["top_losses"] = json!(analyzer.top_losses(&deals, top_losses_limit)); }
if req("loss_sequences") { result["loss_sequences"] = json!(analyzer.loss_sequences(&deals)); }
if req("position_pairs") { result["position_pairs"] = json!(analyzer.position_pairs(&deals)); }
if req("direction_bias") { result["direction_bias"] = json!(analyzer.direction_bias(&deals)); }
if req("streak_analysis") { result["streak_analysis"] = json!(analyzer.streak_analysis(&deals)); }
if req("concurrent_peak") { result["concurrent_peak"] = json!(analyzer.concurrent_peak(&deals)); }
let analysis_path = Path::new(report_dir).join("analysis.json");
fs::write(&analysis_path, serde_json::to_string_pretty(&result)?)?;
Ok(ok_response(json!({
"success": true,
"analysis_file": analysis_path.to_string_lossy(),
"analytics_run": requested.map(|s| s.iter().cloned().collect::<Vec<_>>()).unwrap_or_else(|| vec!["all".to_string()]),
"summary": result,
})))
}
pub async fn handle_compare_baseline(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let report_dir = required_str(args, "report_dir")?;
let baseline_path = Path::new("config/baseline.json");
let metrics_path = Path::new(report_dir).join("metrics.json");
if !baseline_path.exists() {
return Ok(json!({
"content": [{ "type": "text", "text": "No baseline.json found in config/" }],
"isError": false
}));
return Ok(ok_response(json!("No baseline.json found in config/")));
}
let baseline: Value = serde_json::from_str(&fs::read_to_string(baseline_path)?)?;
let current: Value = serde_json::from_str(&fs::read_to_string(metrics_path)?)?;
let comparison = json!({
Ok(ok_response(json!({
"baseline": baseline,
"current": current,
"improvements": {
"profit": current.get("net_profit").and_then(|v| v.as_f64()).unwrap_or(0.0)
"profit": current.get("net_profit").and_then(|v| v.as_f64()).unwrap_or(0.0)
- baseline.get("net_profit").and_then(|v| v.as_f64()).unwrap_or(0.0),
"drawdown": current.get("max_dd_pct").and_then(|v| v.as_f64()).unwrap_or(0.0)
- baseline.get("max_dd_pct").and_then(|v| v.as_f64()).unwrap_or(0.0),
}
});
Ok(json!({
"content": [{ "type": "text", "text": comparison.to_string() }],
"isError": false
}))
})))
}
// === Granular Analytics Handlers ===
// ── Granular analytics handlers ───────────────────────────────────────────────
pub async fn handle_analyze_monthly_pnl(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.monthly_pnl(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"monthly_pnl": result,
}).to_string() }],
"isError": false
}))
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "monthly_pnl": analyzer.monthly_pnl(&deals) })))
}
pub async fn handle_analyze_drawdown_events(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, metrics) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.reconstruct_dd_events(&deals, &metrics);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"drawdown_events": result,
}).to_string() }],
"isError": false
}))
let report_dir = required_str(args, "report_dir")?;
let (deals, metrics, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "drawdown_events": analyzer.reconstruct_dd_events(&deals, &metrics) })))
}
pub async fn handle_analyze_top_losses(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let report_dir = required_str(args, "report_dir")?;
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.top_losses(&deals, limit);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"limit": limit,
"top_losses": result,
}).to_string() }],
"isError": false
}))
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "limit": limit, "top_losses": analyzer.top_losses(&deals, limit) })))
}
pub async fn handle_analyze_loss_sequences(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.loss_sequences(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"loss_sequences": result,
}).to_string() }],
"isError": false
}))
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "loss_sequences": analyzer.loss_sequences(&deals) })))
}
pub async fn handle_analyze_position_pairs(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.position_pairs(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"position_pairs": result,
}).to_string() }],
"isError": false
}))
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "position_pairs": analyzer.position_pairs(&deals) })))
}
pub async fn handle_analyze_direction_bias(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.direction_bias(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"direction_bias": result,
}).to_string() }],
"isError": false
}))
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "direction_bias": analyzer.direction_bias(&deals) })))
}
pub async fn handle_analyze_streaks(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.streak_analysis(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"streak_analysis": result,
}).to_string() }],
"isError": false
}))
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "streak_analysis": analyzer.streak_analysis(&deals) })))
}
pub async fn handle_analyze_concurrent_peak(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.concurrent_peak(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"concurrent_peak": result,
}).to_string() }],
"isError": false
}))
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "concurrent_peak": analyzer.concurrent_peak(&deals) })))
}
// === Deal Query Handlers ===
pub async fn handle_analyze_profit_distribution(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "profit_distribution": analyzer.profit_distribution(&deals) })))
}
pub async fn handle_analyze_time_performance(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "time_performance": analyzer.time_performance(&deals) })))
}
pub async fn handle_analyze_hold_time_distribution(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "hold_time_analysis": analyzer.hold_time_analysis(&deals) })))
}
pub async fn handle_analyze_layer_performance(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "layer_performance": analyzer.layer_performance(&deals) })))
}
pub async fn handle_analyze_volume_vs_profit(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "volume_analysis": analyzer.volume_analysis(&deals) })))
}
pub async fn handle_analyze_costs(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = required_str(args, "report_dir")?;
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "cost_analysis": analyzer.cost_analysis(&deals) })))
}
pub async fn handle_analyze_efficiency(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = required_str(args, "report_dir")?;
let (deals, metrics, analyzer) = prepare_analysis(report_dir)?;
Ok(ok_response(json!({ "success": true, "efficiency_analysis": analyzer.efficiency_analysis(&deals, &metrics) })))
}
// ── Deal query handlers ───────────────────────────────────────────────────────
fn deal_to_json(d: &Deal) -> Value {
json!({
"time": d.time,
"deal": d.deal,
"symbol": d.symbol,
"deal_type": d.deal_type,
"volume": d.volume,
"price": d.price,
"profit": d.profit,
"commission": d.commission,
"swap": d.swap,
"comment": d.comment,
"magic": d.magic,
})
}
fn is_closed_trade(d: &Deal) -> bool {
d.entry.to_lowercase().contains("out") && d.profit != 0.0
}
pub async fn handle_list_deals(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let report_dir = required_str(args, "report_dir")?;
let (deals, _) = load_report_data(report_dir)?;
// Apply filters
let deal_type = args.get("deal_type").and_then(|v| v.as_str());
let deal_type = args.get("deal_type").and_then(|v| v.as_str());
let min_profit = args.get("min_profit").and_then(|v| v.as_f64());
let max_profit = args.get("max_profit").and_then(|v| v.as_f64());
let start_date = args.get("start_date").and_then(|v| v.as_str());
let end_date = args.get("end_date").and_then(|v| v.as_str());
let end_date = args.get("end_date").and_then(|v| v.as_str());
let min_volume = args.get("min_volume").and_then(|v| v.as_f64());
let max_volume = args.get("max_volume").and_then(|v| v.as_f64());
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
let mut filtered: Vec<&Deal> = deals.iter().filter(|d| {
// Only include closed trades with non-zero profit
if !d.entry.to_lowercase().contains("out") || d.profit == 0.0 {
return false;
}
if let Some(dt) = deal_type {
if !d.deal_type.to_lowercase().contains(dt) {
return false;
}
}
if let Some(min) = min_profit {
if d.profit < min {
return false;
}
}
if let Some(max) = max_profit {
if d.profit > max {
return false;
}
}
if let Some(start) = start_date {
if !d.time.starts_with(start) && d.time < start.to_string() {
return false;
}
}
if let Some(end) = end_date {
if d.time > end.to_string() {
return false;
}
}
if let Some(min) = min_volume {
if d.volume < min {
return false;
}
}
if let Some(max) = max_volume {
if d.volume > max {
return false;
}
}
if !is_closed_trade(d) { return false; }
if let Some(dt) = deal_type { if !d.deal_type.to_lowercase().contains(dt) { return false; } }
if let Some(min) = min_profit { if d.profit < min { return false; } }
if let Some(max) = max_profit { if d.profit > max { return false; } }
if let Some(s) = start_date { if d.time.as_str() < s { return false; } }
if let Some(e) = end_date { if d.time.as_str() > e { return false; } }
if let Some(min) = min_volume { if d.volume < min { return false; } }
if let Some(max) = max_volume { if d.volume > max { return false; } }
true
}).collect();
// Sort by time descending
filtered.sort_by(|a, b| b.time.cmp(&a.time));
filtered.truncate(limit);
let deal_list: Vec<Value> = filtered
.iter()
.map(|d| json!({
"time": d.time,
"deal": d.deal,
"symbol": d.symbol,
"deal_type": d.deal_type,
"volume": d.volume,
"price": d.price,
"profit": d.profit,
"commission": d.commission,
"swap": d.swap,
"comment": d.comment,
"magic": d.magic,
}))
.collect();
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"total_deals": deals.len(),
"filtered_count": deal_list.len(),
"deals": deal_list,
}).to_string() }],
"isError": false
}))
Ok(ok_response(json!({
"success": true,
"total_deals": deals.len(),
"filtered_count": filtered.len(),
"deals": filtered.iter().map(|d| deal_to_json(d)).collect::<Vec<_>>(),
})))
}
pub async fn handle_search_deals_by_comment(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let query = args.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("query is required"))?;
let report_dir = required_str(args, "report_dir")?;
let query = required_str(args, "query")?;
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
let (deals, _) = load_report_data(report_dir)?;
let query_lower = query.to_lowercase();
let mut filtered: Vec<&Deal> = deals
.iter()
.filter(|d| {
d.entry.to_lowercase().contains("out")
&& d.profit != 0.0
&& d.comment.to_lowercase().contains(&query_lower)
})
let mut filtered: Vec<&Deal> = deals.iter()
.filter(|d| is_closed_trade(d) && d.comment.to_lowercase().contains(&query_lower))
.collect();
filtered.sort_by(|a, b| b.time.cmp(&a.time));
filtered.truncate(limit);
let deal_list: Vec<Value> = filtered
.iter()
.map(|d| json!({
"time": d.time,
"deal": d.deal,
"symbol": d.symbol,
"deal_type": d.deal_type,
"volume": d.volume,
"profit": d.profit,
"comment": d.comment,
"magic": d.magic,
}))
.collect();
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"query": query,
"matched": deal_list.len(),
"deals": deal_list,
}).to_string() }],
"isError": false
}))
Ok(ok_response(json!({
"success": true,
"query": query,
"matched": filtered.len(),
"deals": filtered.iter().map(|d| deal_to_json(d)).collect::<Vec<_>>(),
})))
}
pub async fn handle_search_deals_by_magic(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let magic = args.get("magic")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("magic is required"))?;
let report_dir = required_str(args, "report_dir")?;
let magic = required_str(args, "magic")?;
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
let (deals, _) = load_report_data(report_dir)?;
let mut filtered: Vec<&Deal> = deals
.iter()
.filter(|d| {
d.entry.to_lowercase().contains("out")
&& d.profit != 0.0
&& d.magic.as_ref().map(|m| m.contains(magic)).unwrap_or(false)
})
let mut filtered: Vec<&Deal> = deals.iter()
.filter(|d| is_closed_trade(d) && d.magic.as_ref().map(|m| m.contains(magic)).unwrap_or(false))
.collect();
filtered.sort_by(|a, b| b.time.cmp(&a.time));
filtered.truncate(limit);
let deal_list: Vec<Value> = filtered
.iter()
.map(|d| json!({
"time": d.time,
"deal": d.deal,
"symbol": d.symbol,
"deal_type": d.deal_type,
"volume": d.volume,
"profit": d.profit,
"comment": d.comment,
"magic": d.magic,
}))
.collect();
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"magic": magic,
"matched": deal_list.len(),
"deals": deal_list,
}).to_string() }],
"isError": false
}))
Ok(ok_response(json!({
"success": true,
"magic": magic,
"matched": filtered.len(),
"deals": filtered.iter().map(|d| deal_to_json(d)).collect::<Vec<_>>(),
})))
}
// === New Analytics Handlers ===
pub async fn handle_analyze_profit_distribution(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.profit_distribution(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"profit_distribution": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_time_performance(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.time_performance(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"time_performance": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_hold_time_distribution(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.hold_time_analysis(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"hold_time_analysis": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_layer_performance(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.layer_performance(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"layer_performance": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_volume_vs_profit(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.volume_analysis(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"volume_analysis": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_costs(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, _) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.cost_analysis(&deals);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"cost_analysis": result,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_analyze_efficiency(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
let (deals, metrics) = load_report_data(report_dir)?;
let analyzer = DealAnalyzer::new();
let result = analyzer.efficiency_analysis(&deals, &metrics);
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"efficiency_analysis": result,
}).to_string() }],
"isError": false
}))
}
// Import Config for analysis module
use crate::models::Config;
// suppress unused warning — err_response is available for future handlers
#[allow(dead_code)]
fn _use_err_response() { let _ = err_response(""); }
+202 -458
View File
@@ -4,212 +4,209 @@ use walkdir::WalkDir;
use crate::compile::MqlCompiler;
use crate::models::Config;
pub async fn handle_list_experts(config: &Config, args: &Value) -> Result<Value> {
let filter = args.get("filter").and_then(|v| v.as_str());
let mut experts = Vec::new();
if let Some(experts_dir) = &config.experts_dir {
for entry in WalkDir::new(experts_dir).max_depth(3) {
if let Ok(entry) = entry {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(name) = path.file_stem() {
let name_str = name.to_string_lossy().to_string();
let is_compiled = path.extension()
.map(|e| e == "ex5")
.unwrap_or(false);
if let Some(filter_str) = filter {
if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) {
continue;
}
}
experts.push(json!({
"name": name_str,
"compiled": is_compiled,
"path": path.to_string_lossy().to_string(),
}));
const BUILTIN_INDICATORS: &[&str] = &[
"Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR",
"Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force",
"Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum",
"OBV", "OsMA", "RSI", "RVI", "SAR", "StdDev", "Stochastic", "WPR",
];
// ── Internal helpers ──────────────────────────────────────────────────────────
/// Walk a MQL directory and return file entries matching an optional filter.
/// `type_label` is included as a `"type"` field when provided (e.g. "custom").
fn scan_mql_dir(dir: Option<&String>, filter: Option<&str>, type_label: Option<&str>) -> Vec<Value> {
let Some(dir) = dir else { return Vec::new() };
let filter_lower = filter.map(|f| f.to_lowercase());
let mut items: Vec<Value> = WalkDir::new(dir)
.max_depth(3)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter_map(|e| {
let path = e.path();
let name = path.file_stem()?.to_string_lossy().into_owned();
if let Some(ref f) = filter_lower {
if !name.to_lowercase().contains(f.as_str()) {
return None;
}
}
}
}
experts.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"count": experts.len(),
"experts": experts,
}).to_string() }],
let is_compiled = path.extension().map(|ext| ext == "ex5").unwrap_or(false);
let mut obj = json!({
"name": name,
"compiled": is_compiled,
"path": path.to_string_lossy().as_ref(),
});
if let Some(t) = type_label {
obj["type"] = json!(t);
}
Some(obj)
})
.collect();
items.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
items
}
fn ok_response(data: Value) -> Value {
json!({
"content": [{ "type": "text", "text": data.to_string() }],
"isError": false
}))
})
}
fn err_response(msg: impl std::fmt::Display) -> Value {
json!({
"content": [{ "type": "text", "text": msg.to_string() }],
"isError": true
})
}
/// Shared implementation for copy_indicator_to_project / copy_script_to_project.
/// `default_fallback` is used as the stem when the source has no filename.
fn copy_mql_to_project(config: &Config, args: &Value, default_fallback: &str) -> Result<Value> {
use std::path::PathBuf;
let source_path = args.get("source_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("source_path is required"))?;
let target_name = args.get("target_name").and_then(|v| v.as_str());
let project_dir = config.project_dir.as_ref()
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok())
.ok_or_else(|| anyhow::anyhow!("Cannot determine project directory"))?;
let source = PathBuf::from(source_path);
if !source.exists() {
return Ok(err_response(
serde_json::to_string(&json!({ "success": false, "error": format!("Source file not found: {}", source_path) })).unwrap_or_default()
));
}
let ext = source.extension().and_then(|e| e.to_str()).unwrap_or("mq5");
let target_filename = match target_name {
Some(name) => format!("{}.{}", name, ext),
None => source.file_name()
.and_then(|n| n.to_str())
.map(|n| n.to_string())
.unwrap_or_else(|| format!("{}.{}", default_fallback, ext)),
};
let destination = project_dir.join(&target_filename);
match std::fs::copy(&source, &destination) {
Ok(bytes) => Ok(ok_response(json!({
"success": true,
"source": source_path,
"destination": destination.to_string_lossy().as_ref(),
"bytes_copied": bytes,
}))),
Err(e) => Ok(err_response(
serde_json::to_string(&json!({ "success": false, "error": format!("Failed to copy file: {}", e) })).unwrap_or_default()
)),
}
}
// ── Public handlers ───────────────────────────────────────────────────────────
pub async fn handle_list_experts(config: &Config, args: &Value) -> Result<Value> {
let filter = args.get("filter").and_then(|v| v.as_str());
let experts = scan_mql_dir(config.experts_dir.as_ref(), filter, None);
Ok(ok_response(json!({
"success": true,
"count": experts.len(),
"experts": experts,
})))
}
pub async fn handle_search_experts(config: &Config, args: &Value) -> Result<Value> {
let pattern = args.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
let pattern_lower = pattern.to_lowercase();
let mut matches = Vec::new();
if let Some(experts_dir) = &config.experts_dir {
for entry in WalkDir::new(experts_dir).max_depth(3) {
if let Ok(entry) = entry {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(name) = path.file_stem() {
let name_str = name.to_string_lossy().to_string();
if name_str.to_lowercase().contains(&pattern_lower) {
let is_compiled = path.extension()
.map(|e| e == "ex5")
.unwrap_or(false);
matches.push(json!({
"name": name_str,
"path": path.to_string_lossy().to_string(),
"compiled": is_compiled,
}));
}
}
}
}
}
matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"pattern": pattern,
"count": matches.len(),
"matches": matches,
}).to_string() }],
"isError": false
}))
let matches = scan_mql_dir(config.experts_dir.as_ref(), Some(pattern), None);
Ok(ok_response(json!({
"success": true,
"pattern": pattern,
"count": matches.len(),
"matches": matches,
})))
}
pub async fn handle_list_indicators(config: &Config, args: &Value) -> Result<Value> {
let filter = args.get("filter").and_then(|v| v.as_str());
let include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false);
let mut indicators = Vec::new();
// List custom indicators
if let Some(indicators_dir) = &config.indicators_dir {
for entry in WalkDir::new(indicators_dir).max_depth(3) {
if let Ok(entry) = entry {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(name) = path.file_stem() {
let name_str = name.to_string_lossy().to_string();
let is_compiled = path.extension()
.map(|e| e == "ex5")
.unwrap_or(false);
if let Some(filter_str) = filter {
if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) {
continue;
}
}
indicators.push(json!({
"name": name_str,
"compiled": is_compiled,
"type": "custom",
"path": path.to_string_lossy().to_string(),
}));
}
}
}
}
// Add built-in indicators if requested
let mut indicators = scan_mql_dir(config.indicators_dir.as_ref(), filter, Some("custom"));
if include_builtin {
let builtin = vec![
"Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR",
"Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force",
"Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum",
"OBV", "OsMA", "RSI", "RVI", "SAR", "StdDev", "Stochastic", "WPR",
];
for name in builtin {
if filter.map(|f| name.to_lowercase().contains(&f.to_lowercase())).unwrap_or(true) {
indicators.push(json!({
"name": name,
"compiled": true,
"type": "builtin",
"path": null,
}));
let filter_lower = filter.map(|f| f.to_lowercase());
for &name in BUILTIN_INDICATORS {
if filter_lower.as_ref().map(|f| name.to_lowercase().contains(f.as_str())).unwrap_or(true) {
indicators.push(json!({ "name": name, "compiled": true, "type": "builtin", "path": null }));
}
}
indicators.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
}
indicators.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"count": indicators.len(),
"indicators": indicators,
"custom_dir": config.indicators_dir.clone(),
}).to_string() }],
"isError": false
}))
Ok(ok_response(json!({
"success": true,
"count": indicators.len(),
"indicators": indicators,
"custom_dir": config.indicators_dir.clone(),
})))
}
pub async fn handle_list_scripts(config: &Config, args: &Value) -> Result<Value> {
let filter = args.get("filter").and_then(|v| v.as_str());
let mut scripts = Vec::new();
if let Some(scripts_dir) = &config.scripts_dir {
for entry in WalkDir::new(scripts_dir).max_depth(3) {
if let Ok(entry) = entry {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(name) = path.file_stem() {
let name_str = name.to_string_lossy().to_string();
let is_compiled = path.extension()
.map(|e| e == "ex5")
.unwrap_or(false);
if let Some(filter_str) = filter {
if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) {
continue;
}
}
scripts.push(json!({
"name": name_str,
"compiled": is_compiled,
"path": path.to_string_lossy().to_string(),
}));
}
let scripts = scan_mql_dir(config.scripts_dir.as_ref(), filter, None);
Ok(ok_response(json!({
"success": true,
"count": scripts.len(),
"scripts": scripts,
"scripts_dir": config.scripts_dir.clone(),
})))
}
pub async fn handle_search_indicators(config: &Config, args: &Value) -> Result<Value> {
let pattern = args.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
let include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false);
let mut matches = scan_mql_dir(config.indicators_dir.as_ref(), Some(pattern), Some("custom"));
if include_builtin {
let pattern_lower = pattern.to_lowercase();
for &name in BUILTIN_INDICATORS {
if name.to_lowercase().contains(&pattern_lower) {
matches.push(json!({ "name": name, "path": null, "type": "builtin", "compiled": true }));
}
}
matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
}
scripts.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"count": scripts.len(),
"scripts": scripts,
"scripts_dir": config.scripts_dir.clone(),
}).to_string() }],
"isError": false
}))
Ok(ok_response(json!({
"success": true,
"pattern": pattern,
"count": matches.len(),
"matches": matches,
})))
}
pub async fn handle_search_scripts(config: &Config, args: &Value) -> Result<Value> {
let pattern = args.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
let matches = scan_mql_dir(config.scripts_dir.as_ref(), Some(pattern), None);
Ok(ok_response(json!({
"success": true,
"pattern": pattern,
"count": matches.len(),
"matches": matches,
})))
}
pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result<Value> {
@@ -218,302 +215,49 @@ pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result<Value> {
let resolved_path: String = if let Some(p) = args.get("expert_path").and_then(|v| v.as_str()) {
p.to_string()
} else if let Some(name) = args.get("expert").and_then(|v| v.as_str()) {
let mut candidates = vec![
PathBuf::from(name).with_extension("mq5"),
];
let mut candidates = vec![PathBuf::from(name).with_extension("mq5")];
if let Some(experts_dir) = &config.experts_dir {
candidates.push(PathBuf::from(experts_dir).join(name).join(format!("{}.mq5", name)));
candidates.push(PathBuf::from(experts_dir).join(format!("{}.mq5", name)));
}
match candidates.into_iter().find(|p| p.exists()) {
Some(p) => p.to_string_lossy().to_string(),
None => return Ok(serde_json::json!({
"content": [{ "type": "text", "text": serde_json::json!({
None => return Ok(err_response(
serde_json::to_string(&json!({
"success": false,
"error": format!("Cannot find {}.mq5 in MT5 Experts dir or current directory", name),
}).to_string() }],
"isError": true
})),
})).unwrap_or_default()
)),
}
} else {
return Err(anyhow::anyhow!("Either 'expert' or 'expert_path' is required"));
};
let compiler = MqlCompiler::new(config.clone());
let expert_path = resolved_path.as_str();
match compiler.compile(&expert_path).await {
Ok(result) => {
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": result.success,
"binary_path": result.ex5_path.map(|p| p.to_string_lossy().to_string()),
"binary_size_bytes": result.binary_size,
"files_synced": result.files_synced,
"warnings": result.warnings.len(),
"errors": result.errors.len(),
"error_list": result.errors,
"warning_list": result.warnings,
}).to_string() }],
"isError": !result.success
}))
}
Err(e) => {
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": false,
"error": format!("Compilation failed: {}", e),
}).to_string() }],
"isError": true
}))
}
match compiler.compile(&resolved_path).await {
Ok(result) => Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": result.success,
"binary_path": result.ex5_path.map(|p| p.to_string_lossy().to_string()),
"binary_size_bytes": result.binary_size,
"files_synced": result.files_synced,
"warnings": result.warnings.len(),
"errors": result.errors.len(),
"error_list": result.errors,
"warning_list": result.warnings,
}).to_string() }],
"isError": !result.success
})),
Err(e) => Ok(err_response(
serde_json::to_string(&json!({ "success": false, "error": format!("Compilation failed: {}", e) })).unwrap_or_default()
)),
}
}
pub async fn handle_search_indicators(config: &Config, args: &Value) -> Result<Value> {
let pattern = args.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
let include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false);
let pattern_lower = pattern.to_lowercase();
let mut matches = Vec::new();
// Search custom indicators recursively
if let Some(indicators_dir) = &config.indicators_dir {
for entry in WalkDir::new(indicators_dir).max_depth(3) {
if let Ok(entry) = entry {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(name) = path.file_stem() {
let name_str = name.to_string_lossy().to_string();
if name_str.to_lowercase().contains(&pattern_lower) {
let is_compiled = path.extension()
.map(|e| e == "ex5")
.unwrap_or(false);
matches.push(json!({
"name": name_str,
"path": path.to_string_lossy().to_string(),
"type": "custom",
"compiled": is_compiled,
}));
}
}
}
}
}
// Search built-in indicators if requested
if include_builtin {
let builtin = vec![
"Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR",
"Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force",
"Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum",
"OBV", "OsMA", "RSI", "RVI", "SAR", "StdDev", "Stochastic", "WPR",
];
for name in builtin {
if name.to_lowercase().contains(&pattern_lower) {
matches.push(json!({
"name": name,
"path": null,
"type": "builtin",
"compiled": true,
}));
}
}
}
matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"pattern": pattern,
"count": matches.len(),
"matches": matches,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_search_scripts(config: &Config, args: &Value) -> Result<Value> {
let pattern = args.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
let pattern_lower = pattern.to_lowercase();
let mut matches = Vec::new();
if let Some(scripts_dir) = &config.scripts_dir {
for entry in WalkDir::new(scripts_dir).max_depth(3) {
if let Ok(entry) = entry {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(name) = path.file_stem() {
let name_str = name.to_string_lossy().to_string();
if name_str.to_lowercase().contains(&pattern_lower) {
let is_compiled = path.extension()
.map(|e| e == "ex5")
.unwrap_or(false);
matches.push(json!({
"name": name_str,
"path": path.to_string_lossy().to_string(),
"compiled": is_compiled,
}));
}
}
}
}
}
matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"pattern": pattern,
"count": matches.len(),
"matches": matches,
}).to_string() }],
"isError": false
}))
}
pub async fn handle_copy_indicator_to_project(config: &Config, args: &Value) -> Result<Value> {
use std::path::PathBuf;
let source_path = args.get("source_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("source_path is required"))?;
let target_name = args.get("target_name").and_then(|v| v.as_str());
// Determine project directory
let project_dir = config.project_dir.as_ref()
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok())
.ok_or_else(|| anyhow::anyhow!("Cannot determine project directory"))?;
let source = PathBuf::from(source_path);
if !source.exists() {
return Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": false,
"error": format!("Source file not found: {}", source_path),
}).to_string() }],
"isError": true
}));
}
// Get extension
let ext = source.extension()
.and_then(|e| e.to_str())
.unwrap_or("mq5");
// Determine target name
let target_filename = match target_name {
Some(name) => format!("{}.{}", name, ext),
None => source.file_name()
.and_then(|n| n.to_str())
.map(|n| n.to_string())
.unwrap_or_else(|| format!("indicator.{}", ext)),
};
let destination = project_dir.join(&target_filename);
// Copy file
match std::fs::copy(&source, &destination) {
Ok(bytes) => {
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"source": source_path,
"destination": destination.to_string_lossy().to_string(),
"bytes_copied": bytes,
}).to_string() }],
"isError": false
}))
}
Err(e) => {
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": false,
"error": format!("Failed to copy file: {}", e),
}).to_string() }],
"isError": true
}))
}
}
copy_mql_to_project(config, args, "indicator")
}
pub async fn handle_copy_script_to_project(config: &Config, args: &Value) -> Result<Value> {
use std::path::PathBuf;
let source_path = args.get("source_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("source_path is required"))?;
let target_name = args.get("target_name").and_then(|v| v.as_str());
// Determine project directory
let project_dir = config.project_dir.as_ref()
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok())
.ok_or_else(|| anyhow::anyhow!("Cannot determine project directory"))?;
let source = PathBuf::from(source_path);
if !source.exists() {
return Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": false,
"error": format!("Source file not found: {}", source_path),
}).to_string() }],
"isError": true
}));
}
// Get extension
let ext = source.extension()
.and_then(|e| e.to_str())
.unwrap_or("mq5");
// Determine target name
let target_filename = match target_name {
Some(name) => format!("{}.{}", name, ext),
None => source.file_name()
.and_then(|n| n.to_str())
.map(|n| n.to_string())
.unwrap_or_else(|| format!("script.{}", ext)),
};
let destination = project_dir.join(&target_filename);
// Copy file
match std::fs::copy(&source, &destination) {
Ok(bytes) => {
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"source": source_path,
"destination": destination.to_string_lossy().to_string(),
"bytes_copied": bytes,
}).to_string() }],
"isError": false
}))
}
Err(e) => {
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": false,
"error": format!("Failed to copy file: {}", e),
}).to_string() }],
"isError": true
}))
}
}
copy_mql_to_project(config, args, "script")
}