Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
836836596f | ||
|
|
f9d266d3ab | ||
|
|
262936a1d5 | ||
|
|
0ea1e779e0 | ||
|
|
a94d75823a | ||
|
|
c7e1a03e1f | ||
|
|
23c67e80c5 | ||
|
|
5db3c7c822 | ||
|
|
5eb753584d | ||
|
|
7c9eeef1c4 | ||
|
|
20a2d3d6e4 | ||
|
|
ec2a22ed30 |
+235
-71
@@ -7,11 +7,13 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Version tag (e.g., v1.31.0)'
|
description: 'Version tag (e.g., v1.32.0)'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# ── Build binaries ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -20,6 +22,16 @@ jobs:
|
|||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
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
|
- name: Build release binary
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|
||||||
@@ -47,6 +59,16 @@ jobs:
|
|||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
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
|
- name: Build release binary
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|
||||||
@@ -66,11 +88,15 @@ jobs:
|
|||||||
name: linux-binary
|
name: linux-binary
|
||||||
path: dist/mcp-mt5-quant-linux-x64.tar.gz
|
path: dist/mcp-mt5-quant-linux-x64.tar.gz
|
||||||
|
|
||||||
|
# ── GitHub Release ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-macos, build-linux]
|
needs: [build-macos, build-linux]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -86,11 +112,11 @@ jobs:
|
|||||||
name: linux-binary
|
name: linux-binary
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Create or Update GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.event.inputs.version || github.ref_name }}
|
tag_name: ${{ env.RELEASE_TAG }}
|
||||||
name: ${{ github.event.inputs.version || github.ref_name }}
|
name: ${{ env.RELEASE_TAG }}
|
||||||
files: |
|
files: |
|
||||||
dist/mcp-mt5-quant-macos-arm64.tar.gz
|
dist/mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
dist/mcp-mt5-quant-linux-x64.tar.gz
|
dist/mcp-mt5-quant-linux-x64.tar.gz
|
||||||
@@ -100,15 +126,23 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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:
|
build-mcp-package:
|
||||||
needs: release
|
needs: release
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
outputs:
|
||||||
|
sha256: ${{ steps.compute-sha256.outputs.sha256 }}
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download macOS artifact
|
- name: Download macOS binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-binary
|
name: macos-binary
|
||||||
@@ -116,36 +150,27 @@ jobs:
|
|||||||
|
|
||||||
- name: Build MCP package
|
- name: Build MCP package
|
||||||
run: |
|
run: |
|
||||||
# Create MCP package structure
|
|
||||||
mkdir -p mcp-package/mcp-server/bin
|
mkdir -p mcp-package/mcp-server/bin
|
||||||
|
cd dist && tar -xzf mcp-mt5-quant-macos-arm64.tar.gz && cd ..
|
||||||
# Extract binary from the tar.gz
|
cp dist/mcp-mt5-quant-macos/mt5-quant mcp-package/mcp-server/bin/
|
||||||
cd dist
|
|
||||||
tar -xzf mcp-mt5-quant-macos-arm64.tar.gz
|
|
||||||
cp mcp-mt5-quant-macos/mt5-quant ../mcp-package/mcp-server/bin/
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Copy config and docs
|
|
||||||
cp -r config mcp-package/
|
cp -r config mcp-package/
|
||||||
cp -r docs mcp-package/
|
cp -r docs mcp-package/
|
||||||
cp README.md mcp-package/
|
cp README.md mcp-package/
|
||||||
cp server.json mcp-package/
|
cp server.json mcp-package/
|
||||||
|
cd mcp-package && tar -czf ../mcp-mt5-quant-macos-arm64.tar.gz . && cd ..
|
||||||
|
|
||||||
# Create tar.gz
|
- name: Compute SHA256
|
||||||
cd mcp-package
|
id: compute-sha256
|
||||||
tar -czf ../mcp-mt5-quant-macos-arm64.tar.gz .
|
run: |
|
||||||
cd ..
|
SHA256=$(shasum -a 256 mcp-mt5-quant-macos-arm64.tar.gz | awk '{print $1}')
|
||||||
|
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
|
||||||
# Calculate SHA256
|
echo "MCP Package SHA256: $SHA256"
|
||||||
shasum -a 256 mcp-mt5-quant-macos-arm64.tar.gz | awk '{print $1}' > mcp-sha256.txt
|
|
||||||
echo "MCP Package SHA256: $(cat mcp-sha256.txt)"
|
|
||||||
|
|
||||||
- name: Upload MCP package to release
|
- name: Upload MCP package to release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.event.inputs.version || github.ref_name }}
|
tag_name: ${{ env.RELEASE_TAG }}
|
||||||
files: |
|
files: mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
mcp-mt5-quant-macos-arm64.tar.gz
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -155,14 +180,81 @@ jobs:
|
|||||||
name: mcp-package
|
name: mcp-package
|
||||||
path: mcp-mt5-quant-macos-arm64.tar.gz
|
path: mcp-mt5-quant-macos-arm64.tar.gz
|
||||||
|
|
||||||
mcp-publish:
|
# ── 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
|
needs: build-mcp-package
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
id-token: write # Required for GitHub OIDC authentication
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Download MCP package
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -171,65 +263,137 @@ jobs:
|
|||||||
path: .
|
path: .
|
||||||
|
|
||||||
- name: Install mcp-publisher
|
- name: Install mcp-publisher
|
||||||
|
id: install-publisher
|
||||||
run: |
|
run: |
|
||||||
# Download mcp-publisher from correct registry repository
|
# Fetch the download URL for linux_amd64 directly from the release assets API
|
||||||
curl -fsSL -o mcp-publisher.tar.gz "https://github.com/modelcontextprotocol/registry/releases/download/v1.0.0/mcp-publisher_1.0.0_linux_amd64.tar.gz" || {
|
# (asset name format changed in v1.6.0 — look it up rather than constructing it)
|
||||||
echo "⚠️ Failed to download mcp-publisher - MCP publish will be skipped"
|
URL=$(curl -sf \
|
||||||
echo "Install manually: https://github.com/modelcontextprotocol/registry/releases"
|
-H "Accept: application/vnd.github+json" \
|
||||||
exit 0
|
"https://api.github.com/repos/modelcontextprotocol/registry/releases/latest" \
|
||||||
}
|
| python3 -c "
|
||||||
tar -xzf mcp-publisher.tar.gz mcp-publisher
|
import sys, json
|
||||||
chmod +x mcp-publisher
|
r = json.load(sys.stdin)
|
||||||
sudo mv mcp-publisher /usr/local/bin/
|
tag = r['tag_name']
|
||||||
rm mcp-publisher.tar.gz
|
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)
|
||||||
|
|
||||||
- name: Publish to MCP Registry
|
if [[ -z "$URL" ]]; then
|
||||||
run: |
|
echo "installed=false" >> "$GITHUB_OUTPUT"
|
||||||
# Check if mcp-publisher is available
|
echo "Could not resolve mcp-publisher download URL"
|
||||||
if ! command -v mcp-publisher &> /dev/null; then
|
else
|
||||||
echo "⚠️ mcp-publisher not found - skipping MCP publish"
|
echo "Downloading: $URL"
|
||||||
echo "To publish manually:"
|
if curl -fsSL -o mcp-publisher.tar.gz "$URL"; then
|
||||||
echo " 1. Install mcp-publisher:"
|
tar -xzf mcp-publisher.tar.gz 2>/dev/null || true
|
||||||
echo " curl -L \"https://github.com/modelcontextprotocol/registry/releases/download/v1.0.0/mcp-publisher_1.0.0_$(uname -s | tr '[:upper:]' '[:lower:]')_amd64.tar.gz\" | tar xz mcp-publisher"
|
BINARY=$(find . -maxdepth 3 -name "mcp-publisher" ! -path "./.git/*" | head -1)
|
||||||
echo " 2. Run: mcp-publisher login github"
|
if [[ -n "$BINARY" && -f "$BINARY" ]]; then
|
||||||
echo " 3. Run: mcp-publisher publish"
|
chmod +x "$BINARY"
|
||||||
exit 0
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=== MCP Registry Publish ==="
|
- name: Validate server.json before publish
|
||||||
# Use GitHub OIDC for authentication (no token needed)
|
id: validate
|
||||||
mcp-publisher login github-oidc
|
run: |
|
||||||
|
python3 - <<'PYEOF'
|
||||||
|
import json, sys
|
||||||
|
|
||||||
# Publish the server
|
with open('server.json') as f:
|
||||||
mcp-publisher publish
|
data = json.load(f)
|
||||||
|
|
||||||
echo "✓ Published to MCP Registry"
|
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:
|
release-info:
|
||||||
needs: [mcp-publish]
|
needs: [mcp-publish]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Display Release Commands
|
- name: Summary
|
||||||
run: |
|
run: |
|
||||||
echo ""
|
echo ""
|
||||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ 🎉 RELEASE COMPLETED SUCCESSFULLY! 🎉 ║"
|
echo "║ Release ${RELEASE_TAG} complete! "
|
||||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📦 GitHub Release: https://github.com/masdevid/mt5-quant/releases/tag/${{ github.event.inputs.version || github.ref_name }}"
|
echo "GitHub Release:"
|
||||||
echo "📦 MCP Registry: io.github.masdevid/mt5-quant"
|
echo " https://github.com/masdevid/mt5-quant/releases/tag/${RELEASE_TAG}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "═══════════════════════════════════════════════════════════════════"
|
echo "MCP Registry: io.github.masdevid/mt5-quant"
|
||||||
echo " MCP CLIENT REGISTRATION COMMANDS"
|
|
||||||
echo "═══════════════════════════════════════════════════════════════════"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "▶️ CLAUDE CODE:"
|
echo "── MCP Client Registration ──────────────────────────────────"
|
||||||
echo " claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "▶️ WINDSURF:"
|
echo "Claude Code:"
|
||||||
echo ' echo \'{"mcpServers": {"io.github.masdevid/mt5-quant": {"command": "~/.local/bin/mt5-quant", "disabled": false, "registry": "io.github.masdevid/mt5-quant"}}}\' >> ~/.codeium/windsurf/mcp_config.json'
|
echo " claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant"
|
||||||
echo ""
|
echo ""
|
||||||
echo "▶️ CURSOR / VSCODE:"
|
echo "Windsurf (~/.codeium/windsurf/mcp_config.json):"
|
||||||
echo ' mkdir -p ~/.cursor && echo \'{"mcpServers": {"mt5-quant": {"command": "~/.local/bin/mt5-quant"}}}\' > ~/.cursor/mcp.json'
|
echo ' {"mcpServers": {"io.github.masdevid/mt5-quant": {"command": "~/.local/bin/mt5-quant"}}}'
|
||||||
echo ""
|
echo ""
|
||||||
echo "═══════════════════════════════════════════════════════════════════"
|
echo "Cursor / VS Code (~/.cursor/mcp.json):"
|
||||||
|
echo ' {"mcpServers": {"mt5-quant": {"command": "~/.local/bin/mt5-quant"}}}'
|
||||||
|
echo ""
|
||||||
|
echo "─────────────────────────────────────────────────────────────"
|
||||||
|
|||||||
+2
-4
@@ -20,14 +20,12 @@ config/baseline.json
|
|||||||
config/backtest_history.json
|
config/backtest_history.json
|
||||||
|
|
||||||
# Claude Code (personal, not for repo)
|
# Claude Code (personal, not for repo)
|
||||||
CLAUDE.md
|
/CLAUDE.md
|
||||||
|
.claude/
|
||||||
|
|
||||||
# Windsurf (local workflows, not for repo)
|
# Windsurf (local workflows, not for repo)
|
||||||
.windsurf/
|
.windsurf/
|
||||||
|
|
||||||
# GitHub Actions (local workflows, not for repo)
|
|
||||||
.github/workflows/
|
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
---
|
|
||||||
description: Release workflow - bump version, build, and publish MCP server
|
|
||||||
tags: [release, publish, workflow]
|
|
||||||
---
|
|
||||||
|
|
||||||
# MT5-Quant Release Workflow
|
|
||||||
|
|
||||||
Steps to release a new version of the MCP server.
|
|
||||||
|
|
||||||
## 1. Pre-Release Checklist
|
|
||||||
|
|
||||||
- [ ] All features implemented and tested
|
|
||||||
- [ ] Documentation updated (README.md, MCP_TOOLS.md)
|
|
||||||
- [ ] Tool count verified: `grep -r "pub fn tool_" src/tools/definitions/ | wc -l`
|
|
||||||
- [ ] Version bumped in Cargo.toml
|
|
||||||
- [ ] Build passes: `cargo build --release`
|
|
||||||
|
|
||||||
## 2. Update Documentation
|
|
||||||
|
|
||||||
// turbo
|
|
||||||
```bash
|
|
||||||
grep -r "pub fn tool_" src/tools/definitions/ | wc -l
|
|
||||||
```
|
|
||||||
|
|
||||||
Update these files with correct tool counts:
|
|
||||||
- `README.md` - header and "MCP Tools (N)" section
|
|
||||||
- `docs/MCP_TOOLS.md` - "documents X of Y total tools" line
|
|
||||||
|
|
||||||
## 3. Bump Version
|
|
||||||
|
|
||||||
Edit `Cargo.toml` and update the version:
|
|
||||||
```toml
|
|
||||||
version = "X.Y.Z"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Build Release
|
|
||||||
|
|
||||||
// turbo
|
|
||||||
```bash
|
|
||||||
bash scripts/build-release.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates: `dist/mcp-mt5-quant-{platform}.tar.gz`
|
|
||||||
|
|
||||||
Calculate SHA256 for server.json:
|
|
||||||
```bash
|
|
||||||
shasum -a 256 dist/mcp-mt5-quant-macos-arm64.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Update MCP Server Configuration
|
|
||||||
|
|
||||||
Update both `server.json` files with:
|
|
||||||
- New version
|
|
||||||
- New SHA256 hash from step 4
|
|
||||||
- Updated download URL
|
|
||||||
- Tool count in description
|
|
||||||
|
|
||||||
Files to update:
|
|
||||||
- `server.json`
|
|
||||||
- `mcp-package/server.json`
|
|
||||||
|
|
||||||
Then copy the release package:
|
|
||||||
```bash
|
|
||||||
cp dist/mcp-mt5-quant-macos-arm64.tar.gz mcp-package/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Create Git Tag & Commit
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "Release vX.Y.Z - brief description"
|
|
||||||
git tag vX.Y.Z
|
|
||||||
git push origin vX.Y.Z
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Create GitHub Release (Manual)
|
|
||||||
|
|
||||||
1. Go to GitHub → Releases → Draft new release
|
|
||||||
2. Choose tag: `vX.Y.Z`
|
|
||||||
3. Release title: `vX.Y.Z`
|
|
||||||
4. Attach binary: `dist/mcp-mt5-quant-macos-arm64.tar.gz`
|
|
||||||
5. Publish release
|
|
||||||
|
|
||||||
## 8. Install Binary to System Path
|
|
||||||
|
|
||||||
Install the binary to a single location for all MCP clients:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create local bin if needed
|
|
||||||
mkdir -p ~/.local/bin
|
|
||||||
|
|
||||||
# Install binary
|
|
||||||
cp target/release/mt5-quant ~/.local/bin/
|
|
||||||
|
|
||||||
# Or system-wide (requires sudo)
|
|
||||||
# sudo cp target/release/mt5-quant /usr/local/bin/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Installation Path:** `~/.local/bin/mt5-quant` (single location for all clients)
|
|
||||||
|
|
||||||
## 9. MCP Registration
|
|
||||||
|
|
||||||
Use the installed binary path (not project directory):
|
|
||||||
|
|
||||||
### Claude Code
|
|
||||||
```bash
|
|
||||||
claude mcp add mt5-quant -- ~/.local/bin/mt5-quant
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windsurf
|
|
||||||
Edit `~/.codeium/windsurf/mcp_config.json`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "~/.local/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### VS Code / Cursor
|
|
||||||
Edit `~/.cursor/mcp.json` or `.vscode/mcp.json`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "~/.local/bin/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10. MCP Client Registration
|
|
||||||
|
|
||||||
After installing the binary, register with your MCP clients:
|
|
||||||
|
|
||||||
### Windsurf
|
|
||||||
|
|
||||||
// turbo
|
|
||||||
```bash
|
|
||||||
# Add to Windsurf MCP config
|
|
||||||
cat >> ~/.codeium/windsurf/mcp_config.json << 'EOF'
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"io.github.masdevid/mt5-quant": {
|
|
||||||
"command": "~/.local/bin/mt5-quant",
|
|
||||||
"disabled": false,
|
|
||||||
"registry": "io.github.masdevid/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Claude Code
|
|
||||||
|
|
||||||
// turbo
|
|
||||||
```bash
|
|
||||||
# Register with Claude Code
|
|
||||||
claude mcp add io.github.masdevid/mt5-quant -- ~/.local/bin/mt5-quant
|
|
||||||
|
|
||||||
# Or with custom name
|
|
||||||
claude mcp add mt5-quant -- ~/.local/bin/mt5-quant
|
|
||||||
```
|
|
||||||
|
|
||||||
### VS Code / Cursor
|
|
||||||
|
|
||||||
// turbo
|
|
||||||
```bash
|
|
||||||
# Add to Cursor MCP config
|
|
||||||
mkdir -p ~/.cursor
|
|
||||||
cat > ~/.cursor/mcp.json << 'EOF'
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt5-quant": {
|
|
||||||
"command": "~/.local/bin/mt5-quant",
|
|
||||||
"env": {
|
|
||||||
"MT5_MCP_HOME": "~/.config/mt5-quant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
## 11. Post-Release Verification
|
|
||||||
|
|
||||||
// turbo
|
|
||||||
```bash
|
|
||||||
# Test the binary
|
|
||||||
./target/release/mt5-quant --help
|
|
||||||
|
|
||||||
# Verify tool count
|
|
||||||
./target/release/mt5-quant 2>&1 | head -20
|
|
||||||
|
|
||||||
# Check MCP registration
|
|
||||||
claude mcp list
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Release Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Full release cycle
|
|
||||||
vim Cargo.toml # bump version
|
|
||||||
bash scripts/build-release.sh
|
|
||||||
git add . && git commit -m "Release vX.Y.Z"
|
|
||||||
git tag vX.Y.Z && git push origin vX.Y.Z
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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
@@ -481,7 +481,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mt5-quant"
|
name = "mt5-quant"
|
||||||
version = "1.31.0"
|
version = "1.31.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mt5-quant"
|
name = "mt5-quant"
|
||||||
version = "1.31.0"
|
version = "1.31.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "MT5-Quant MCP Server - Exposes MT5 backtest and optimization tools via MCP"
|
description = "MT5-Quant MCP Server - Exposes MT5 backtest and optimization tools via MCP"
|
||||||
authors = ["masdevid <masdevid@example.com>"]
|
authors = ["masdevid <masdevid@example.com>"]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Claude: [compile → clean → backtest → analyze 1,847 deals]
|
|||||||
### 1. Download & Setup
|
### 1. Download & Setup
|
||||||
|
|
||||||
```bash
|
```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
|
tar -xzf mt5.tar.gz
|
||||||
bash scripts/setup.sh
|
bash scripts/setup.sh
|
||||||
```
|
```
|
||||||
@@ -40,7 +40,7 @@ bash scripts/setup.sh
|
|||||||
| **Windsurf** | Edit `~/.codeium/windsurf/mcp_config.json` | [WINDSURF.md →](docs/WINDSURF.md) |
|
| **Windsurf** | Edit `~/.codeium/windsurf/mcp_config.json` | [WINDSURF.md →](docs/WINDSURF.md) |
|
||||||
| **Cursor** | Edit `~/.cursor/mcp.json` or use Settings → MCP | [CURSOR.md →](docs/CURSOR.md) |
|
| **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) |
|
| **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`.
|
> **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 |
|
| [WINDSURF.md](docs/WINDSURF.md) | Windsurf IDE setup |
|
||||||
| [CURSOR.md](docs/CURSOR.md) | Cursor IDE setup |
|
| [CURSOR.md](docs/CURSOR.md) | Cursor IDE setup |
|
||||||
| [VSCODE.md](docs/VSCODE.md) | VS Code 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 |
|
| [CONFIG.md](docs/CONFIG.md) | Configuration reference |
|
||||||
| [TOOLS.md](docs/MCP_TOOLS.md) | All 87 tools documented |
|
| [TOOLS.md](docs/MCP_TOOLS.md) | All 87 tools documented |
|
||||||
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design and internals |
|
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design and internals |
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Apple Silicon)
|
# 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
|
tar -xzf mt5-quant.tar.gz
|
||||||
|
|
||||||
# Linux (x64)
|
# 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
|
tar -xzf mt5-quant.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -6,19 +6,19 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Apple Silicon)
|
# 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
|
tar -xzf mt5-quant.tar.gz
|
||||||
|
|
||||||
# Linux (x64)
|
# 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
|
tar -xzf mt5-quant.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option B: Build from Source
|
### Option B: Build from Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/masdevid/mt5-mcp
|
git clone https://github.com/masdevid/mt5-quant
|
||||||
cd mt5-mcp
|
cd mt5-quant
|
||||||
bash scripts/build-rust.sh
|
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.
|
Or run `MCP: Add Server` from Command Palette.
|
||||||
|
|
||||||
### Antigravity
|
### Claude Desktop
|
||||||
|
|
||||||
1. Open Agent panel → Click "..." menu → MCP Servers
|
1. Open Agent panel → Click "..." menu → MCP Servers
|
||||||
2. Click "Manage 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
|
## 5. Verify Setup
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Apple Silicon)
|
# 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
|
tar -xzf mt5-quant.tar.gz
|
||||||
|
|
||||||
# Linux (x64)
|
# 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
|
tar -xzf mt5-quant.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (Apple Silicon)
|
# 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
|
tar -xzf mt5-quant.tar.gz
|
||||||
|
|
||||||
# Linux (x64)
|
# 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
|
tar -xzf mt5-quant.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Executable
+200
@@ -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
|
||||||
+5
-5
@@ -7,13 +7,13 @@
|
|||||||
"url": "https://github.com/masdevid/mt5-quant",
|
"url": "https://github.com/masdevid/mt5-quant",
|
||||||
"source": "github"
|
"source": "github"
|
||||||
},
|
},
|
||||||
"version": "1.31.0",
|
"version": "1.31.3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"registryType": "mcpbPackageType",
|
"registryType": "mcpb",
|
||||||
"version": "1.31.0",
|
"version": "1.31.3",
|
||||||
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.31.0/mcp-mt5-quant-macos-arm64.tar.gz",
|
"identifier": "https://github.com/masdevid/mt5-quant/releases/download/v1.31.3/mcp-mt5-quant-macos-arm64.tar.gz",
|
||||||
"fileSha256": "b23ab29823c14bc2c968869ec9ce0787e60fe531f40f19e6492d0b3314f8223d",
|
"fileSha256": "TBD_CI_WILL_UPDATE",
|
||||||
"transport": {
|
"transport": {
|
||||||
"type": "stdio"
|
"type": "stdio"
|
||||||
},
|
},
|
||||||
|
|||||||
+196
-488
@@ -6,8 +6,30 @@ use std::path::Path;
|
|||||||
use crate::analytics::DealAnalyzer;
|
use crate::analytics::DealAnalyzer;
|
||||||
use crate::models::deals::Deal;
|
use crate::models::deals::Deal;
|
||||||
use crate::models::metrics::Metrics;
|
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)> {
|
fn load_report_data(report_dir: &str) -> Result<(Vec<Deal>, Metrics)> {
|
||||||
let deals_csv = Path::new(report_dir).join("deals.csv");
|
let deals_csv = Path::new(report_dir).join("deals.csv");
|
||||||
let metrics_json = Path::new(report_dir).join("metrics.json");
|
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 deals = read_deals_from_csv(&deals_csv)?;
|
||||||
|
|
||||||
let metrics = if metrics_json.exists() {
|
let metrics = if metrics_json.exists() {
|
||||||
let content = fs::read_to_string(&metrics_json)?;
|
serde_json::from_str(&fs::read_to_string(&metrics_json)?)?
|
||||||
serde_json::from_str(&content)?
|
|
||||||
} else {
|
} else {
|
||||||
Metrics::default()
|
Metrics::default()
|
||||||
};
|
};
|
||||||
@@ -28,66 +48,9 @@ fn load_report_data(report_dir: &str) -> Result<(Vec<Deal>, Metrics)> {
|
|||||||
Ok((deals, metrics))
|
Ok((deals, metrics))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result<Value> {
|
fn prepare_analysis(report_dir: &str) -> Result<(Vec<Deal>, Metrics, DealAnalyzer)> {
|
||||||
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 (deals, metrics) = load_report_data(report_dir)?;
|
||||||
let analyzer = DealAnalyzer::new();
|
Ok((deals, metrics, 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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_deals_from_csv(path: &Path) -> Result<Vec<Deal>> {
|
fn read_deals_from_csv(path: &Path) -> Result<Vec<Deal>> {
|
||||||
@@ -122,25 +85,57 @@ fn read_deals_from_csv(path: &Path) -> Result<Vec<Deal>> {
|
|||||||
Ok(deals)
|
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> {
|
pub async fn handle_compare_baseline(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
|
||||||
|
|
||||||
let baseline_path = Path::new("config/baseline.json");
|
let baseline_path = Path::new("config/baseline.json");
|
||||||
let metrics_path = Path::new(report_dir).join("metrics.json");
|
let metrics_path = Path::new(report_dir).join("metrics.json");
|
||||||
|
|
||||||
if !baseline_path.exists() {
|
if !baseline_path.exists() {
|
||||||
return Ok(json!({
|
return Ok(ok_response(json!("No baseline.json found in config/")));
|
||||||
"content": [{ "type": "text", "text": "No baseline.json found in config/" }],
|
|
||||||
"isError": false
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let baseline: Value = serde_json::from_str(&fs::read_to_string(baseline_path)?)?;
|
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 current: Value = serde_json::from_str(&fs::read_to_string(metrics_path)?)?;
|
||||||
|
|
||||||
let comparison = json!({
|
Ok(ok_response(json!({
|
||||||
"baseline": baseline,
|
"baseline": baseline,
|
||||||
"current": current,
|
"current": current,
|
||||||
"improvements": {
|
"improvements": {
|
||||||
@@ -149,492 +144,205 @@ pub async fn handle_compare_baseline(_config: &Config, args: &Value) -> Result<V
|
|||||||
"drawdown": current.get("max_dd_pct").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),
|
- 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> {
|
pub async fn handle_analyze_monthly_pnl(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
Ok(ok_response(json!({ "success": true, "monthly_pnl": analyzer.monthly_pnl(&deals) })))
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_analyze_drawdown_events(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_analyze_drawdown_events(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let (deals, metrics, analyzer) = prepare_analysis(report_dir)?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
Ok(ok_response(json!({ "success": true, "drawdown_events": analyzer.reconstruct_dd_events(&deals, &metrics) })))
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_analyze_top_losses(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_analyze_top_losses(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
|
||||||
|
|
||||||
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
||||||
let (deals, _) = load_report_data(report_dir)?;
|
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
|
||||||
let analyzer = DealAnalyzer::new();
|
Ok(ok_response(json!({ "success": true, "limit": limit, "top_losses": analyzer.top_losses(&deals, limit) })))
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_analyze_loss_sequences(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_analyze_loss_sequences(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
Ok(ok_response(json!({ "success": true, "loss_sequences": analyzer.loss_sequences(&deals) })))
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_analyze_position_pairs(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_analyze_position_pairs(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
Ok(ok_response(json!({ "success": true, "position_pairs": analyzer.position_pairs(&deals) })))
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_analyze_direction_bias(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_analyze_direction_bias(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
Ok(ok_response(json!({ "success": true, "direction_bias": analyzer.direction_bias(&deals) })))
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_analyze_streaks(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_analyze_streaks(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
Ok(ok_response(json!({ "success": true, "streak_analysis": analyzer.streak_analysis(&deals) })))
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_analyze_concurrent_peak(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_analyze_concurrent_peak(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let (deals, _, analyzer) = prepare_analysis(report_dir)?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("report_dir is required"))?;
|
Ok(ok_response(json!({ "success": true, "concurrent_peak": analyzer.concurrent_peak(&deals) })))
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 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> {
|
pub async fn handle_list_deals(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "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 (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 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 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 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 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 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 limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
|
||||||
|
|
||||||
let mut filtered: Vec<&Deal> = deals.iter().filter(|d| {
|
let mut filtered: Vec<&Deal> = deals.iter().filter(|d| {
|
||||||
// Only include closed trades with non-zero profit
|
if !is_closed_trade(d) { return false; }
|
||||||
if !d.entry.to_lowercase().contains("out") || d.profit == 0.0 {
|
if let Some(dt) = deal_type { if !d.deal_type.to_lowercase().contains(dt) { return false; } }
|
||||||
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(dt) = deal_type {
|
if let Some(e) = end_date { if d.time.as_str() > e { return false; } }
|
||||||
if !d.deal_type.to_lowercase().contains(dt) {
|
if let Some(min) = min_volume { if d.volume < min { return false; } }
|
||||||
return false;
|
if let Some(max) = max_volume { if d.volume > max { 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
// Sort by time descending
|
|
||||||
filtered.sort_by(|a, b| b.time.cmp(&a.time));
|
filtered.sort_by(|a, b| b.time.cmp(&a.time));
|
||||||
filtered.truncate(limit);
|
filtered.truncate(limit);
|
||||||
|
|
||||||
let deal_list: Vec<Value> = filtered
|
Ok(ok_response(json!({
|
||||||
.iter()
|
"success": true,
|
||||||
.map(|d| json!({
|
"total_deals": deals.len(),
|
||||||
"time": d.time,
|
"filtered_count": filtered.len(),
|
||||||
"deal": d.deal,
|
"deals": filtered.iter().map(|d| deal_to_json(d)).collect::<Vec<_>>(),
|
||||||
"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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_search_deals_by_comment(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_search_deals_by_comment(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let query = required_str(args, "query")?;
|
||||||
.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 limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
|
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
|
||||||
|
|
||||||
let (deals, _) = load_report_data(report_dir)?;
|
let (deals, _) = load_report_data(report_dir)?;
|
||||||
|
|
||||||
let query_lower = query.to_lowercase();
|
let query_lower = query.to_lowercase();
|
||||||
let mut filtered: Vec<&Deal> = deals
|
|
||||||
.iter()
|
let mut filtered: Vec<&Deal> = deals.iter()
|
||||||
.filter(|d| {
|
.filter(|d| is_closed_trade(d) && d.comment.to_lowercase().contains(&query_lower))
|
||||||
d.entry.to_lowercase().contains("out")
|
|
||||||
&& d.profit != 0.0
|
|
||||||
&& d.comment.to_lowercase().contains(&query_lower)
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
filtered.sort_by(|a, b| b.time.cmp(&a.time));
|
filtered.sort_by(|a, b| b.time.cmp(&a.time));
|
||||||
filtered.truncate(limit);
|
filtered.truncate(limit);
|
||||||
|
|
||||||
let deal_list: Vec<Value> = filtered
|
Ok(ok_response(json!({
|
||||||
.iter()
|
"success": true,
|
||||||
.map(|d| json!({
|
"query": query,
|
||||||
"time": d.time,
|
"matched": filtered.len(),
|
||||||
"deal": d.deal,
|
"deals": filtered.iter().map(|d| deal_to_json(d)).collect::<Vec<_>>(),
|
||||||
"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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_search_deals_by_magic(_config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_search_deals_by_magic(_config: &Config, args: &Value) -> Result<Value> {
|
||||||
let report_dir = args.get("report_dir")
|
let report_dir = required_str(args, "report_dir")?;
|
||||||
.and_then(|v| v.as_str())
|
let magic = required_str(args, "magic")?;
|
||||||
.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 limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
|
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
|
||||||
|
|
||||||
let (deals, _) = load_report_data(report_dir)?;
|
let (deals, _) = load_report_data(report_dir)?;
|
||||||
|
|
||||||
let mut filtered: Vec<&Deal> = deals
|
let mut filtered: Vec<&Deal> = deals.iter()
|
||||||
.iter()
|
.filter(|d| is_closed_trade(d) && d.magic.as_ref().map(|m| m.contains(magic)).unwrap_or(false))
|
||||||
.filter(|d| {
|
|
||||||
d.entry.to_lowercase().contains("out")
|
|
||||||
&& d.profit != 0.0
|
|
||||||
&& d.magic.as_ref().map(|m| m.contains(magic)).unwrap_or(false)
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
filtered.sort_by(|a, b| b.time.cmp(&a.time));
|
filtered.sort_by(|a, b| b.time.cmp(&a.time));
|
||||||
filtered.truncate(limit);
|
filtered.truncate(limit);
|
||||||
|
|
||||||
let deal_list: Vec<Value> = filtered
|
Ok(ok_response(json!({
|
||||||
.iter()
|
"success": true,
|
||||||
.map(|d| json!({
|
"magic": magic,
|
||||||
"time": d.time,
|
"matched": filtered.len(),
|
||||||
"deal": d.deal,
|
"deals": filtered.iter().map(|d| deal_to_json(d)).collect::<Vec<_>>(),
|
||||||
"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
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === New Analytics Handlers ===
|
// suppress unused warning — err_response is available for future handlers
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn handle_analyze_profit_distribution(_config: &Config, args: &Value) -> Result<Value> {
|
fn _use_err_response() { let _ = err_response(""); }
|
||||||
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;
|
|
||||||
|
|||||||
+188
-444
@@ -4,212 +4,209 @@ use walkdir::WalkDir;
|
|||||||
use crate::compile::MqlCompiler;
|
use crate::compile::MqlCompiler;
|
||||||
use crate::models::Config;
|
use crate::models::Config;
|
||||||
|
|
||||||
pub async fn handle_list_experts(config: &Config, args: &Value) -> Result<Value> {
|
const BUILTIN_INDICATORS: &[&str] = &[
|
||||||
let filter = args.get("filter").and_then(|v| v.as_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",
|
||||||
|
];
|
||||||
|
|
||||||
let mut experts = Vec::new();
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if let Some(experts_dir) = &config.experts_dir {
|
/// Walk a MQL directory and return file entries matching an optional filter.
|
||||||
for entry in WalkDir::new(experts_dir).max_depth(3) {
|
/// `type_label` is included as a `"type"` field when provided (e.g. "custom").
|
||||||
if let Ok(entry) = entry {
|
fn scan_mql_dir(dir: Option<&String>, filter: Option<&str>, type_label: Option<&str>) -> Vec<Value> {
|
||||||
let path = entry.path();
|
let Some(dir) = dir else { return Vec::new() };
|
||||||
if !path.is_file() {
|
let filter_lower = filter.map(|f| f.to_lowercase());
|
||||||
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 {
|
let mut items: Vec<Value> = WalkDir::new(dir)
|
||||||
if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) {
|
.max_depth(3)
|
||||||
continue;
|
.into_iter()
|
||||||
}
|
.filter_map(|e| e.ok())
|
||||||
}
|
.filter(|e| e.path().is_file())
|
||||||
|
.filter_map(|e| {
|
||||||
experts.push(json!({
|
let path = e.path();
|
||||||
"name": name_str,
|
let name = path.file_stem()?.to_string_lossy().into_owned();
|
||||||
"compiled": is_compiled,
|
if let Some(ref f) = filter_lower {
|
||||||
"path": path.to_string_lossy().to_string(),
|
if !name.to_lowercase().contains(f.as_str()) {
|
||||||
}));
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
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()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
experts.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
|
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)),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(json!({
|
let destination = project_dir.join(&target_filename);
|
||||||
"content": [{ "type": "text", "text": json!({
|
|
||||||
|
match std::fs::copy(&source, &destination) {
|
||||||
|
Ok(bytes) => Ok(ok_response(json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"count": experts.len(),
|
"source": source_path,
|
||||||
"experts": experts,
|
"destination": destination.to_string_lossy().as_ref(),
|
||||||
}).to_string() }],
|
"bytes_copied": bytes,
|
||||||
"isError": false
|
}))),
|
||||||
}))
|
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> {
|
pub async fn handle_search_experts(config: &Config, args: &Value) -> Result<Value> {
|
||||||
let pattern = args.get("pattern")
|
let pattern = args.get("pattern")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
|
.ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
|
||||||
|
let matches = scan_mql_dir(config.experts_dir.as_ref(), Some(pattern), None);
|
||||||
let pattern_lower = pattern.to_lowercase();
|
Ok(ok_response(json!({
|
||||||
let mut matches = Vec::new();
|
"success": true,
|
||||||
|
"pattern": pattern,
|
||||||
if let Some(experts_dir) = &config.experts_dir {
|
"count": matches.len(),
|
||||||
for entry in WalkDir::new(experts_dir).max_depth(3) {
|
"matches": matches,
|
||||||
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_list_indicators(config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_list_indicators(config: &Config, args: &Value) -> Result<Value> {
|
||||||
let filter = args.get("filter").and_then(|v| v.as_str());
|
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 include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
|
||||||
let mut indicators = Vec::new();
|
let mut indicators = scan_mql_dir(config.indicators_dir.as_ref(), filter, Some("custom"));
|
||||||
|
|
||||||
// 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
|
|
||||||
if include_builtin {
|
if include_builtin {
|
||||||
let builtin = vec![
|
let filter_lower = filter.map(|f| f.to_lowercase());
|
||||||
"Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR",
|
for &name in BUILTIN_INDICATORS {
|
||||||
"Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force",
|
if filter_lower.as_ref().map(|f| name.to_lowercase().contains(f.as_str())).unwrap_or(true) {
|
||||||
"Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum",
|
indicators.push(json!({ "name": name, "compiled": true, "type": "builtin", "path": null }));
|
||||||
"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,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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(ok_response(json!({
|
||||||
|
"success": true,
|
||||||
Ok(json!({
|
"count": indicators.len(),
|
||||||
"content": [{ "type": "text", "text": json!({
|
"indicators": indicators,
|
||||||
"success": true,
|
"custom_dir": config.indicators_dir.clone(),
|
||||||
"count": indicators.len(),
|
})))
|
||||||
"indicators": indicators,
|
|
||||||
"custom_dir": config.indicators_dir.clone(),
|
|
||||||
}).to_string() }],
|
|
||||||
"isError": false
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_list_scripts(config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_list_scripts(config: &Config, args: &Value) -> Result<Value> {
|
||||||
let filter = args.get("filter").and_then(|v| v.as_str());
|
let filter = args.get("filter").and_then(|v| v.as_str());
|
||||||
|
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(),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
let mut scripts = Vec::new();
|
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);
|
||||||
|
|
||||||
if let Some(scripts_dir) = &config.scripts_dir {
|
let mut matches = scan_mql_dir(config.indicators_dir.as_ref(), Some(pattern), Some("custom"));
|
||||||
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 include_builtin {
|
||||||
if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) {
|
let pattern_lower = pattern.to_lowercase();
|
||||||
continue;
|
for &name in BUILTIN_INDICATORS {
|
||||||
}
|
if name.to_lowercase().contains(&pattern_lower) {
|
||||||
}
|
matches.push(json!({ "name": name, "path": null, "type": "builtin", "compiled": true }));
|
||||||
|
|
||||||
scripts.push(json!({
|
|
||||||
"name": name_str,
|
|
||||||
"compiled": is_compiled,
|
|
||||||
"path": path.to_string_lossy().to_string(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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(ok_response(json!({
|
||||||
|
"success": true,
|
||||||
|
"pattern": pattern,
|
||||||
|
"count": matches.len(),
|
||||||
|
"matches": matches,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
Ok(json!({
|
pub async fn handle_search_scripts(config: &Config, args: &Value) -> Result<Value> {
|
||||||
"content": [{ "type": "text", "text": json!({
|
let pattern = args.get("pattern")
|
||||||
"success": true,
|
.and_then(|v| v.as_str())
|
||||||
"count": scripts.len(),
|
.ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
|
||||||
"scripts": scripts,
|
let matches = scan_mql_dir(config.scripts_dir.as_ref(), Some(pattern), None);
|
||||||
"scripts_dir": config.scripts_dir.clone(),
|
Ok(ok_response(json!({
|
||||||
}).to_string() }],
|
"success": true,
|
||||||
"isError": false
|
"pattern": pattern,
|
||||||
}))
|
"count": matches.len(),
|
||||||
|
"matches": matches,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result<Value> {
|
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()) {
|
let resolved_path: String = if let Some(p) = args.get("expert_path").and_then(|v| v.as_str()) {
|
||||||
p.to_string()
|
p.to_string()
|
||||||
} else if let Some(name) = args.get("expert").and_then(|v| v.as_str()) {
|
} else if let Some(name) = args.get("expert").and_then(|v| v.as_str()) {
|
||||||
let mut candidates = vec![
|
let mut candidates = vec![PathBuf::from(name).with_extension("mq5")];
|
||||||
PathBuf::from(name).with_extension("mq5"),
|
|
||||||
];
|
|
||||||
if let Some(experts_dir) = &config.experts_dir {
|
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(name).join(format!("{}.mq5", name)));
|
||||||
candidates.push(PathBuf::from(experts_dir).join(format!("{}.mq5", name)));
|
candidates.push(PathBuf::from(experts_dir).join(format!("{}.mq5", name)));
|
||||||
}
|
}
|
||||||
match candidates.into_iter().find(|p| p.exists()) {
|
match candidates.into_iter().find(|p| p.exists()) {
|
||||||
Some(p) => p.to_string_lossy().to_string(),
|
Some(p) => p.to_string_lossy().to_string(),
|
||||||
None => return Ok(serde_json::json!({
|
None => return Ok(err_response(
|
||||||
"content": [{ "type": "text", "text": serde_json::json!({
|
serde_json::to_string(&json!({
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": format!("Cannot find {}.mq5 in MT5 Experts dir or current directory", name),
|
"error": format!("Cannot find {}.mq5 in MT5 Experts dir or current directory", name),
|
||||||
}).to_string() }],
|
})).unwrap_or_default()
|
||||||
"isError": true
|
)),
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!("Either 'expert' or 'expert_path' is required"));
|
return Err(anyhow::anyhow!("Either 'expert' or 'expert_path' is required"));
|
||||||
};
|
};
|
||||||
|
|
||||||
let compiler = MqlCompiler::new(config.clone());
|
let compiler = MqlCompiler::new(config.clone());
|
||||||
let expert_path = resolved_path.as_str();
|
match compiler.compile(&resolved_path).await {
|
||||||
|
Ok(result) => Ok(json!({
|
||||||
match compiler.compile(&expert_path).await {
|
"content": [{ "type": "text", "text": json!({
|
||||||
Ok(result) => {
|
"success": result.success,
|
||||||
Ok(json!({
|
"binary_path": result.ex5_path.map(|p| p.to_string_lossy().to_string()),
|
||||||
"content": [{ "type": "text", "text": json!({
|
"binary_size_bytes": result.binary_size,
|
||||||
"success": result.success,
|
"files_synced": result.files_synced,
|
||||||
"binary_path": result.ex5_path.map(|p| p.to_string_lossy().to_string()),
|
"warnings": result.warnings.len(),
|
||||||
"binary_size_bytes": result.binary_size,
|
"errors": result.errors.len(),
|
||||||
"files_synced": result.files_synced,
|
"error_list": result.errors,
|
||||||
"warnings": result.warnings.len(),
|
"warning_list": result.warnings,
|
||||||
"errors": result.errors.len(),
|
}).to_string() }],
|
||||||
"error_list": result.errors,
|
"isError": !result.success
|
||||||
"warning_list": result.warnings,
|
})),
|
||||||
}).to_string() }],
|
Err(e) => Ok(err_response(
|
||||||
"isError": !result.success
|
serde_json::to_string(&json!({ "success": false, "error": format!("Compilation failed: {}", e) })).unwrap_or_default()
|
||||||
}))
|
)),
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
Ok(json!({
|
|
||||||
"content": [{ "type": "text", "text": json!({
|
|
||||||
"success": false,
|
|
||||||
"error": format!("Compilation failed: {}", e),
|
|
||||||
}).to_string() }],
|
|
||||||
"isError": true
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
pub async fn handle_copy_indicator_to_project(config: &Config, args: &Value) -> Result<Value> {
|
||||||
use std::path::PathBuf;
|
copy_mql_to_project(config, args, "indicator")
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_copy_script_to_project(config: &Config, args: &Value) -> Result<Value> {
|
pub async fn handle_copy_script_to_project(config: &Config, args: &Value) -> Result<Value> {
|
||||||
use std::path::PathBuf;
|
copy_mql_to_project(config, args, "script")
|
||||||
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user