Migración desde trading-platform/apps/mcp-mt4-connector - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
980e56de20
31
.env.example
Normal file
31
.env.example
Normal file
@ -0,0 +1,31 @@
|
||||
# MCP MT4 Connector Configuration
|
||||
# Copy this file to .env and configure values
|
||||
|
||||
# ==========================================
|
||||
# Server Configuration
|
||||
# ==========================================
|
||||
PORT=3605
|
||||
NODE_ENV=development
|
||||
|
||||
# ==========================================
|
||||
# MT4 Gateway Connection
|
||||
# ==========================================
|
||||
# Host where mt4-gateway is running
|
||||
MT4_GATEWAY_HOST=localhost
|
||||
# Port of the mt4-gateway service
|
||||
MT4_GATEWAY_PORT=8081
|
||||
# Authentication token for mt4-gateway
|
||||
MT4_GATEWAY_AUTH_TOKEN=your-secret-token-here
|
||||
|
||||
# ==========================================
|
||||
# Request Configuration
|
||||
# ==========================================
|
||||
# Timeout for requests to MT4 Gateway (ms)
|
||||
REQUEST_TIMEOUT=10000
|
||||
# Maximum retries for failed requests
|
||||
MAX_RETRIES=3
|
||||
|
||||
# ==========================================
|
||||
# Logging
|
||||
# ==========================================
|
||||
LOG_LEVEL=info
|
||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
277
README.md
Normal file
277
README.md
Normal file
@ -0,0 +1,277 @@
|
||||
# MCP MT4 Connector
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Date:** 2026-01-04
|
||||
**System:** Trading Platform + NEXUS v3.4 + SIMCO
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
MCP Server that exposes MetaTrader 4 (MT4) trading capabilities as tools for AI agents. This service enables AI agents to:
|
||||
- Query account information
|
||||
- Monitor open positions
|
||||
- Execute trades (BUY/SELL)
|
||||
- Manage positions (modify SL/TP, close)
|
||||
- Get real-time price quotes
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Navigate to the project
|
||||
cd /home/isem/workspace-v1/projects/trading-platform/apps/mcp-mt4-connector
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
# Edit .env with your MT4 Gateway credentials
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | MCP Server port | 3605 |
|
||||
| `MT4_GATEWAY_HOST` | MT4 Gateway hostname | localhost |
|
||||
| `MT4_GATEWAY_PORT` | MT4 Gateway port | 8081 |
|
||||
| `MT4_GATEWAY_AUTH_TOKEN` | Authentication token | secret |
|
||||
| `REQUEST_TIMEOUT` | Request timeout (ms) | 10000 |
|
||||
| `LOG_LEVEL` | Logging level | info |
|
||||
|
||||
### Example .env
|
||||
```env
|
||||
PORT=3605
|
||||
MT4_GATEWAY_HOST=localhost
|
||||
MT4_GATEWAY_PORT=8081
|
||||
MT4_GATEWAY_AUTH_TOKEN=your-secure-token
|
||||
REQUEST_TIMEOUT=10000
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Start Server
|
||||
|
||||
```bash
|
||||
# Development (with hot reload)
|
||||
npm run dev
|
||||
|
||||
# Production
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3605/health
|
||||
```
|
||||
|
||||
### List Available Tools
|
||||
|
||||
```bash
|
||||
curl http://localhost:3605/tools
|
||||
```
|
||||
|
||||
### Execute a Tool
|
||||
|
||||
```bash
|
||||
# Get account info
|
||||
curl -X POST http://localhost:3605/tools/mt4_get_account \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {}}'
|
||||
|
||||
# Get price quote
|
||||
curl -X POST http://localhost:3605/tools/mt4_get_quote \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "XAUUSD"}}'
|
||||
|
||||
# Execute trade
|
||||
curl -X POST http://localhost:3605/tools/mt4_execute_trade \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "XAUUSD", "action": "buy", "lots": 0.1}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP Tools Available
|
||||
|
||||
| Tool | Description | Risk |
|
||||
|------|-------------|------|
|
||||
| `mt4_get_account` | Get account balance, equity, margin | Low |
|
||||
| `mt4_get_positions` | List open positions | Low |
|
||||
| `mt4_get_quote` | Get current bid/ask price | Low |
|
||||
| `mt4_execute_trade` | Execute BUY/SELL order | HIGH |
|
||||
| `mt4_close_position` | Close a position | HIGH |
|
||||
| `mt4_modify_position` | Modify SL/TP | Medium |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mcp-mt4-connector/
|
||||
├── README.md # This file
|
||||
├── package.json # Dependencies
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── .env.example # Environment template
|
||||
├── .gitignore # Git ignore rules
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md # Architecture documentation
|
||||
│ └── MCP-TOOLS-SPEC.md # Detailed tool specifications
|
||||
└── src/
|
||||
├── index.ts # Server entry point
|
||||
├── tools/
|
||||
│ ├── index.ts # Tool exports
|
||||
│ ├── account.ts # mt4_get_account
|
||||
│ ├── positions.ts # mt4_get_positions, mt4_close_position
|
||||
│ ├── trading.ts # mt4_execute_trade, mt4_modify_position
|
||||
│ └── quotes.ts # mt4_get_quote
|
||||
└── services/
|
||||
└── mt4-client.ts # MT4 Gateway HTTP client
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Type Check
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Claude
|
||||
|
||||
### MCP Configuration
|
||||
|
||||
Add to your Claude/MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mt4": {
|
||||
"url": "http://localhost:3605",
|
||||
"transport": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Agent Prompts
|
||||
|
||||
```
|
||||
"Check my MT4 account balance"
|
||||
→ Uses mt4_get_account
|
||||
|
||||
"What's the current gold price?"
|
||||
→ Uses mt4_get_quote({ symbol: "XAUUSD" })
|
||||
|
||||
"Buy 0.1 lots of XAUUSD with stop loss at 2640"
|
||||
→ Uses mt4_execute_trade({ symbol: "XAUUSD", action: "buy", lots: 0.1, stopLoss: 2640 })
|
||||
|
||||
"Close my profitable gold positions"
|
||||
→ Uses mt4_get_positions({ symbol: "XAUUSD" }) + mt4_close_position for each
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime
|
||||
- `express` - HTTP server
|
||||
- `axios` - HTTP client
|
||||
- `zod` - Input validation
|
||||
- `dotenv` - Environment configuration
|
||||
- `@modelcontextprotocol/sdk` - MCP protocol
|
||||
|
||||
### Development
|
||||
- `typescript` - Type safety
|
||||
- `ts-node-dev` - Development server
|
||||
- `jest` - Testing framework
|
||||
- `eslint` - Code linting
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **MT4 Gateway** running on configured host:port
|
||||
2. **MT4 Terminal** connected with EA Bridge active
|
||||
3. **Node.js** >= 18.0.0
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot connect to MT4 Gateway
|
||||
```bash
|
||||
# Check if mt4-gateway is running
|
||||
curl http://localhost:8081/status
|
||||
|
||||
# Verify environment variables
|
||||
cat .env | grep MT4
|
||||
```
|
||||
|
||||
### Tool execution fails
|
||||
```bash
|
||||
# Check health endpoint for dependency status
|
||||
curl http://localhost:3605/health
|
||||
|
||||
# Check server logs
|
||||
npm run dev # Logs will show in console
|
||||
```
|
||||
|
||||
### Invalid parameters error
|
||||
```bash
|
||||
# Verify tool schema
|
||||
curl http://localhost:3605/tools/mt4_execute_trade
|
||||
|
||||
# Check parameter names match schema
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [MCP Protocol](https://modelcontextprotocol.io)
|
||||
- MT4 Gateway: `apps/mt4-gateway/`
|
||||
- SIMCO-MCP: `orchestration/directivas/simco/SIMCO-MCP.md`
|
||||
- Architecture: `docs/ARCHITECTURE.md`
|
||||
- Tool Specs: `docs/MCP-TOOLS-SPEC.md`
|
||||
|
||||
---
|
||||
|
||||
**Maintained by:** @PERFIL_MCP_DEVELOPER
|
||||
**Project:** Trading Platform
|
||||
272
docs/ARCHITECTURE.md
Normal file
272
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,272 @@
|
||||
# MCP MT4 Connector - Architecture
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Date:** 2026-01-04
|
||||
**System:** Trading Platform
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP MT4 Connector is a Model Context Protocol (MCP) server that exposes MetaTrader 4 trading capabilities as tools that AI agents can use. It acts as a bridge between MCP-compatible AI systems (like Claude) and the MT4 trading terminal.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ AI Agent (Claude) │
|
||||
│ │
|
||||
│ "Execute a buy order for 0.1 lots of XAUUSD with SL at 2640" │
|
||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
||||
│ MCP Protocol
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ MCP MT4 Connector (Port 3605) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Express Server │ │
|
||||
│ │ │ │
|
||||
│ │ /health - Health check endpoint │ │
|
||||
│ │ /tools - List available tools │ │
|
||||
│ │ /tools/:name - Execute specific tool │ │
|
||||
│ │ /mcp/* - MCP protocol endpoints │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Tool Handlers │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
|
||||
│ │ │ account.ts │ │positions.ts │ │ trading.ts │ │ quotes.ts │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ MT4 Client Service │ │
|
||||
│ │ │ │
|
||||
│ │ - HTTP client wrapper for mt4-gateway │ │
|
||||
│ │ - Request/response handling │ │
|
||||
│ │ - Error management │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
||||
│ HTTP/REST
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ MT4 Gateway (Port 8081) │
|
||||
│ │
|
||||
│ Python service that communicates with MT4 EA Bridge │
|
||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
||||
│ Local Socket/HTTP
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ MT4 Terminal + EA Bridge │
|
||||
│ │
|
||||
│ Windows MT4 with Expert Advisor running │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Details
|
||||
|
||||
### 1. Express Server (`src/index.ts`)
|
||||
|
||||
The main entry point that:
|
||||
- Hosts the MCP server on port 3605
|
||||
- Provides REST endpoints for tool execution
|
||||
- Implements MCP protocol endpoints
|
||||
- Handles health checks and service discovery
|
||||
|
||||
**Endpoints:**
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/health` | GET | Health check with MT4 connection status |
|
||||
| `/tools` | GET | List all available MCP tools |
|
||||
| `/tools/:name` | GET | Get specific tool schema |
|
||||
| `/tools/:name` | POST | Execute tool with parameters |
|
||||
| `/mcp/initialize` | POST | MCP initialization handshake |
|
||||
| `/mcp/tools/list` | POST | MCP tool listing |
|
||||
| `/mcp/tools/call` | POST | MCP tool execution |
|
||||
|
||||
### 2. Tool Handlers (`src/tools/`)
|
||||
|
||||
Individual tool implementations following the MCP tool pattern:
|
||||
|
||||
| File | Tools | Description |
|
||||
|------|-------|-------------|
|
||||
| `account.ts` | `mt4_get_account` | Account information retrieval |
|
||||
| `positions.ts` | `mt4_get_positions`, `mt4_close_position` | Position management |
|
||||
| `trading.ts` | `mt4_execute_trade`, `mt4_modify_position` | Trade execution |
|
||||
| `quotes.ts` | `mt4_get_quote` | Price data retrieval |
|
||||
|
||||
Each tool handler:
|
||||
- Defines Zod validation schemas
|
||||
- Implements the core logic
|
||||
- Formats responses for MCP protocol
|
||||
- Handles errors gracefully
|
||||
|
||||
### 3. MT4 Client Service (`src/services/mt4-client.ts`)
|
||||
|
||||
HTTP client wrapper that:
|
||||
- Manages connection to mt4-gateway
|
||||
- Handles authentication (Bearer token)
|
||||
- Provides typed interfaces for all operations
|
||||
- Manages request timeouts and retries
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Example: Execute Trade
|
||||
|
||||
```
|
||||
1. Agent Request
|
||||
POST /mcp/tools/call
|
||||
{
|
||||
"name": "mt4_execute_trade",
|
||||
"arguments": {
|
||||
"symbol": "XAUUSD",
|
||||
"action": "buy",
|
||||
"lots": 0.1,
|
||||
"stopLoss": 2640,
|
||||
"takeProfit": 2680
|
||||
}
|
||||
}
|
||||
|
||||
2. Tool Handler (trading.ts)
|
||||
- Validates input with Zod schema
|
||||
- Checks MT4 connection status
|
||||
- Validates SL/TP logic
|
||||
- Calls MT4Client.executeTrade()
|
||||
|
||||
3. MT4 Client Service
|
||||
- Formats request payload
|
||||
- Sends HTTP POST to mt4-gateway
|
||||
- Receives and parses response
|
||||
|
||||
4. MT4 Gateway
|
||||
- Forwards to EA Bridge
|
||||
- EA executes trade on MT4
|
||||
- Returns result
|
||||
|
||||
5. Response to Agent
|
||||
{
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "Trade Executed Successfully\n..."
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | 3605 | MCP server port |
|
||||
| `MT4_GATEWAY_HOST` | localhost | MT4 Gateway host |
|
||||
| `MT4_GATEWAY_PORT` | 8081 | MT4 Gateway port |
|
||||
| `MT4_GATEWAY_AUTH_TOKEN` | secret | Authentication token |
|
||||
| `REQUEST_TIMEOUT` | 10000 | Request timeout (ms) |
|
||||
| `LOG_LEVEL` | info | Logging level |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
1. **Connection Errors**
|
||||
- MT4 Gateway unreachable
|
||||
- MT4 Terminal disconnected
|
||||
|
||||
2. **Validation Errors**
|
||||
- Invalid parameters (Zod)
|
||||
- Invalid SL/TP configuration
|
||||
|
||||
3. **Trading Errors**
|
||||
- Insufficient margin
|
||||
- Market closed
|
||||
- Invalid symbol
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message description"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Authentication**
|
||||
- Bearer token for mt4-gateway communication
|
||||
- No external network exposure by default
|
||||
|
||||
2. **Validation**
|
||||
- All inputs validated with Zod schemas
|
||||
- Type-safe throughout the codebase
|
||||
|
||||
3. **Rate Limiting**
|
||||
- Consider adding rate limiting for production
|
||||
- Respect MT4 order frequency limits
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `express` | HTTP server |
|
||||
| `axios` | HTTP client |
|
||||
| `zod` | Input validation |
|
||||
| `dotenv` | Environment configuration |
|
||||
| `@modelcontextprotocol/sdk` | MCP protocol types |
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Docker (Future)
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY dist ./dist
|
||||
EXPOSE 3605
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- MCP Protocol: https://modelcontextprotocol.io
|
||||
- MT4 Bridge Client: `apps/mt4-gateway/src/providers/mt4_bridge_client.py`
|
||||
- Trading Platform: `projects/trading-platform/`
|
||||
- SIMCO-MCP Directive: `orchestration/directivas/simco/SIMCO-MCP.md`
|
||||
428
docs/MCP-TOOLS-SPEC.md
Normal file
428
docs/MCP-TOOLS-SPEC.md
Normal file
@ -0,0 +1,428 @@
|
||||
# MCP MT4 Connector - Tools Specification
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Date:** 2026-01-04
|
||||
**Total Tools:** 6
|
||||
|
||||
---
|
||||
|
||||
## Tool Overview
|
||||
|
||||
| Tool Name | Description | Risk Level |
|
||||
|-----------|-------------|------------|
|
||||
| `mt4_get_account` | Get account information | Low |
|
||||
| `mt4_get_positions` | List open positions | Low |
|
||||
| `mt4_get_quote` | Get current price quote | Low |
|
||||
| `mt4_execute_trade` | Execute market order | HIGH |
|
||||
| `mt4_close_position` | Close a position | HIGH |
|
||||
| `mt4_modify_position` | Modify SL/TP | Medium |
|
||||
|
||||
---
|
||||
|
||||
## mt4_get_account
|
||||
|
||||
### Description
|
||||
Retrieves comprehensive account information from the connected MT4 terminal including balance, equity, margin, leverage, and broker details.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| - | - | - | No parameters required |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"balance": 10000.00,
|
||||
"equity": 10250.50,
|
||||
"margin": 500.00,
|
||||
"freeMargin": 9750.50,
|
||||
"marginLevel": 2050.10,
|
||||
"profit": 250.50,
|
||||
"currency": "USD",
|
||||
"leverage": 100,
|
||||
"name": "Demo Account",
|
||||
"server": "ICMarkets-Demo",
|
||||
"company": "IC Markets"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Get account info
|
||||
const result = await mt4_get_account({});
|
||||
|
||||
// Response content:
|
||||
// MT4 Account Information
|
||||
// =======================
|
||||
// Account Name: Demo Account
|
||||
// Server: ICMarkets-Demo
|
||||
// Broker: IC Markets
|
||||
// Leverage: 1:100
|
||||
//
|
||||
// Financial Summary
|
||||
// -----------------
|
||||
// Balance: 10000.00 USD
|
||||
// Equity: 10250.50 USD
|
||||
// Profit/Loss: +250.50 USD
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | MT4 terminal is not connected | Check MT4 Gateway connection |
|
||||
|
||||
---
|
||||
|
||||
## mt4_get_positions
|
||||
|
||||
### Description
|
||||
Lists all currently open trading positions from the MT4 terminal. Can optionally filter by symbol.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `symbol` | string | No | Filter positions by symbol (e.g., XAUUSD) |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"positions": [
|
||||
{
|
||||
"ticket": 123456,
|
||||
"symbol": "XAUUSD",
|
||||
"type": "buy",
|
||||
"lots": 0.10,
|
||||
"openPrice": 2650.50,
|
||||
"currentPrice": 2655.00,
|
||||
"stopLoss": 2640.00,
|
||||
"takeProfit": 2680.00,
|
||||
"profit": 45.00,
|
||||
"swap": -1.20,
|
||||
"openTime": "2026-01-04T10:30:00Z",
|
||||
"magic": 12345,
|
||||
"comment": "AI Signal"
|
||||
}
|
||||
],
|
||||
"totalProfit": 45.00,
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Get all positions
|
||||
const result = await mt4_get_positions({});
|
||||
|
||||
// Get only XAUUSD positions
|
||||
const goldPositions = await mt4_get_positions({ symbol: "XAUUSD" });
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | MT4 terminal is not connected | Check MT4 Gateway connection |
|
||||
|
||||
---
|
||||
|
||||
## mt4_get_quote
|
||||
|
||||
### Description
|
||||
Retrieves the current bid/ask prices for a trading symbol. Also calculates the spread in points/pips.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `symbol` | string | Yes | Trading symbol (e.g., XAUUSD, EURUSD) |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"symbol": "XAUUSD",
|
||||
"bid": 2650.50,
|
||||
"ask": 2650.80,
|
||||
"spread": 0.30,
|
||||
"timestamp": "2026-01-04T12:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Get gold price
|
||||
const quote = await mt4_get_quote({ symbol: "XAUUSD" });
|
||||
|
||||
// Response content:
|
||||
// Price Quote: XAUUSD
|
||||
// =========================
|
||||
// Bid: 2650.50
|
||||
// Ask: 2650.80
|
||||
// Spread: 0.30 (3.0 pips)
|
||||
// Time: 2026-01-04T12:00:00.000Z
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | No quote data available for {symbol} | Verify symbol is available on broker |
|
||||
|
||||
---
|
||||
|
||||
## mt4_execute_trade
|
||||
|
||||
### Description
|
||||
Opens a new trading position with a market order. Supports BUY and SELL orders with optional stop loss and take profit levels.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `symbol` | string | Yes | Trading symbol (e.g., XAUUSD) |
|
||||
| `action` | string | Yes | Trade direction: "buy" or "sell" |
|
||||
| `lots` | number | Yes | Volume in lots (e.g., 0.01, 0.1, 1.0) |
|
||||
| `stopLoss` | number | No | Stop loss price level |
|
||||
| `takeProfit` | number | No | Take profit price level |
|
||||
| `slippage` | number | No | Maximum slippage in points (default: 3) |
|
||||
| `magic` | number | No | Magic number for EA identification (default: 12345) |
|
||||
| `comment` | string | No | Order comment (max 31 chars) |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"ticket": 123456,
|
||||
"message": "Order placed successfully",
|
||||
"symbol": "XAUUSD",
|
||||
"action": "buy",
|
||||
"lots": 0.1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Simple buy order
|
||||
const result = await mt4_execute_trade({
|
||||
symbol: "XAUUSD",
|
||||
action: "buy",
|
||||
lots: 0.1
|
||||
});
|
||||
|
||||
// Buy with risk management
|
||||
const result = await mt4_execute_trade({
|
||||
symbol: "XAUUSD",
|
||||
action: "buy",
|
||||
lots: 0.1,
|
||||
stopLoss: 2640.00,
|
||||
takeProfit: 2680.00,
|
||||
comment: "AI Signal - Gold Long"
|
||||
});
|
||||
|
||||
// Sell order
|
||||
const result = await mt4_execute_trade({
|
||||
symbol: "EURUSD",
|
||||
action: "sell",
|
||||
lots: 0.5,
|
||||
stopLoss: 1.1050,
|
||||
takeProfit: 1.0900
|
||||
});
|
||||
```
|
||||
|
||||
### Validation Rules
|
||||
- For BUY orders: stopLoss must be below takeProfit
|
||||
- For SELL orders: stopLoss must be above takeProfit
|
||||
- Lots must be positive and reasonable (max 100)
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | For BUY orders, stop loss must be below take profit | Fix SL/TP levels |
|
||||
| - | For SELL orders, stop loss must be above take profit | Fix SL/TP levels |
|
||||
| - | Trade execution failed | Check margin, market hours |
|
||||
|
||||
---
|
||||
|
||||
## mt4_close_position
|
||||
|
||||
### Description
|
||||
Closes an open position by ticket number. Can optionally close only a partial volume.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `ticket` | number | Yes | Position ticket number to close |
|
||||
| `lots` | number | No | Partial volume to close (default: close all) |
|
||||
| `slippage` | number | No | Maximum slippage in points (default: 3) |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"ticket": 123456,
|
||||
"message": "Position closed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Close entire position
|
||||
const result = await mt4_close_position({
|
||||
ticket: 123456
|
||||
});
|
||||
|
||||
// Close partial position (0.5 of 1.0 lots)
|
||||
const result = await mt4_close_position({
|
||||
ticket: 123456,
|
||||
lots: 0.5
|
||||
});
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | Position with ticket {x} not found | Verify ticket number |
|
||||
| - | Requested lots exceeds position size | Reduce lots parameter |
|
||||
|
||||
---
|
||||
|
||||
## mt4_modify_position
|
||||
|
||||
### Description
|
||||
Modifies the stop loss and/or take profit levels of an existing open position.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `ticket` | number | Yes | Position ticket number to modify |
|
||||
| `stopLoss` | number | No | New stop loss price level |
|
||||
| `takeProfit` | number | No | New take profit price level |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"ticket": 123456,
|
||||
"message": "Position modified successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Set both SL and TP
|
||||
const result = await mt4_modify_position({
|
||||
ticket: 123456,
|
||||
stopLoss: 2640.00,
|
||||
takeProfit: 2680.00
|
||||
});
|
||||
|
||||
// Update only take profit (trailing)
|
||||
const result = await mt4_modify_position({
|
||||
ticket: 123456,
|
||||
takeProfit: 2700.00
|
||||
});
|
||||
|
||||
// Set only stop loss (risk management)
|
||||
const result = await mt4_modify_position({
|
||||
ticket: 123456,
|
||||
stopLoss: 2650.00
|
||||
});
|
||||
```
|
||||
|
||||
### Validation Rules
|
||||
- At least one of stopLoss or takeProfit must be provided
|
||||
- For BUY positions: stopLoss must be below takeProfit
|
||||
- For SELL positions: stopLoss must be above takeProfit
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | At least one of stopLoss or takeProfit must be provided | Add SL or TP |
|
||||
| - | Position with ticket {x} not found | Verify ticket number |
|
||||
| - | For BUY positions, stop loss must be below take profit | Fix SL/TP levels |
|
||||
|
||||
---
|
||||
|
||||
## Common Error Responses
|
||||
|
||||
### Connection Error
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "MT4 terminal is not connected"
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Error
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Validation error",
|
||||
"details": [
|
||||
{
|
||||
"path": ["symbol"],
|
||||
"message": "Required"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Trading Error
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Trade execution failed: Insufficient margin"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples with AI Agent
|
||||
|
||||
### Scenario 1: Check Account and Open Trade
|
||||
```
|
||||
Agent: "Check my account balance and if equity is above 10000, buy 0.1 lots of XAUUSD"
|
||||
|
||||
1. Call mt4_get_account({})
|
||||
2. Parse response, check equity > 10000
|
||||
3. Call mt4_execute_trade({ symbol: "XAUUSD", action: "buy", lots: 0.1 })
|
||||
```
|
||||
|
||||
### Scenario 2: Risk Management
|
||||
```
|
||||
Agent: "Set stop loss at 2640 and take profit at 2680 for my gold position"
|
||||
|
||||
1. Call mt4_get_positions({ symbol: "XAUUSD" })
|
||||
2. Get ticket number from response
|
||||
3. Call mt4_modify_position({ ticket: 123456, stopLoss: 2640, takeProfit: 2680 })
|
||||
```
|
||||
|
||||
### Scenario 3: Close Profitable Trades
|
||||
```
|
||||
Agent: "Close all profitable gold positions"
|
||||
|
||||
1. Call mt4_get_positions({ symbol: "XAUUSD" })
|
||||
2. Filter positions where profit > 0
|
||||
3. For each: Call mt4_close_position({ ticket: ticketNumber })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 0.1.0 | 2026-01-04 | Initial release with 6 core tools |
|
||||
7170
package-lock.json
generated
Normal file
7170
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
package.json
Normal file
53
package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "mcp-mt4-connector",
|
||||
"version": "0.1.0",
|
||||
"description": "MCP Server for MT4 trading operations via EA Bridge",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev --respawn src/index.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"lint:fix": "eslint src/**/*.ts --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"health-check": "curl -s http://localhost:${PORT:-3605}/health || echo 'Server not running'"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"anthropic",
|
||||
"claude",
|
||||
"mt4",
|
||||
"metatrader",
|
||||
"trading",
|
||||
"forex"
|
||||
],
|
||||
"author": "Trading Platform Trading Platform",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"axios": "^1.6.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"eslint": "^8.54.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
291
src/index.ts
Normal file
291
src/index.ts
Normal file
@ -0,0 +1,291 @@
|
||||
/**
|
||||
* MCP Server: MT4 Connector
|
||||
*
|
||||
* Exposes MT4 trading capabilities as MCP tools for AI agents.
|
||||
* Communicates with mt4-gateway service to execute trading operations.
|
||||
*
|
||||
* @version 0.1.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import dotenv from 'dotenv';
|
||||
import { mcpToolSchemas, toolHandlers } from './tools';
|
||||
import { getMT4Client } from './services/mt4-client';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3605;
|
||||
const SERVICE_NAME = 'mcp-mt4-connector';
|
||||
const VERSION = '0.1.0';
|
||||
|
||||
// ==========================================
|
||||
// Middleware
|
||||
// ==========================================
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, _res: Response, next: NextFunction) => {
|
||||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Health & Status Endpoints
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Health check endpoint
|
||||
*/
|
||||
app.get('/health', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
const mt4Connected = await client.isConnected();
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
dependencies: {
|
||||
mt4Gateway: mt4Connected ? 'connected' : 'disconnected',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.json({
|
||||
status: 'degraded',
|
||||
service: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
dependencies: {
|
||||
mt4Gateway: 'error',
|
||||
},
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* List available MCP tools
|
||||
*/
|
||||
app.get('/tools', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
tools: mcpToolSchemas,
|
||||
count: mcpToolSchemas.length,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get specific tool schema
|
||||
*/
|
||||
app.get('/tools/:toolName', (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const tool = mcpToolSchemas.find(t => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
res.status(404).json({
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: mcpToolSchemas.map(t => t.name),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(tool);
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MCP Tool Execution Endpoints
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Execute an MCP tool
|
||||
* POST /tools/:toolName
|
||||
* Body: { parameters: {...} }
|
||||
*/
|
||||
app.post('/tools/:toolName', async (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const { parameters = {} } = req.body;
|
||||
|
||||
// Validate tool exists
|
||||
const handler = toolHandlers[toolName];
|
||||
if (!handler) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: Object.keys(toolHandlers),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[${new Date().toISOString()}] Executing tool: ${toolName}`);
|
||||
console.log(`Parameters: ${JSON.stringify(parameters)}`);
|
||||
|
||||
const result = await handler(parameters);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
tool: toolName,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Tool error: ${toolName}`, error);
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: (error as { issues: unknown[] }).issues,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MCP Protocol Endpoints (Standard)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* MCP Initialize
|
||||
* Returns server capabilities
|
||||
*/
|
||||
app.post('/mcp/initialize', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCP List Tools
|
||||
* Returns all available tools in MCP format
|
||||
*/
|
||||
app.post('/mcp/tools/list', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
tools: mcpToolSchemas.map(tool => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCP Call Tool
|
||||
* Execute a tool with parameters
|
||||
*/
|
||||
app.post('/mcp/tools/call', async (req: Request, res: Response) => {
|
||||
const { name, arguments: args = {} } = req.body;
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'invalid_request',
|
||||
message: 'Tool name is required',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = toolHandlers[name];
|
||||
if (!handler) {
|
||||
res.status(404).json({
|
||||
error: {
|
||||
code: 'unknown_tool',
|
||||
message: `Tool '${name}' not found`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(args);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
// Handle Zod validation errors
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'invalid_params',
|
||||
message: 'Invalid tool parameters',
|
||||
data: (error as { issues: unknown[] }).issues,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'internal_error',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Error Handler
|
||||
// ==========================================
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.error(`[${new Date().toISOString()}] Unhandled error:`, err);
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Start Server
|
||||
// ==========================================
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log('');
|
||||
console.log('╔══════════════════════════════════════════════════════════╗');
|
||||
console.log('║ MCP MT4 Connector - Trading Platform ║');
|
||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
||||
console.log(`║ Service: ${SERVICE_NAME.padEnd(45)}║`);
|
||||
console.log(`║ Version: ${VERSION.padEnd(45)}║`);
|
||||
console.log(`║ Port: ${String(PORT).padEnd(48)}║`);
|
||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
||||
console.log('║ Endpoints: ║');
|
||||
console.log(`║ - Health: http://localhost:${PORT}/health`.padEnd(63) + '║');
|
||||
console.log(`║ - Tools: http://localhost:${PORT}/tools`.padEnd(63) + '║');
|
||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
||||
console.log('║ MCP Tools Available: ║');
|
||||
mcpToolSchemas.forEach(tool => {
|
||||
console.log(`║ - ${tool.name.padEnd(54)}║`);
|
||||
});
|
||||
console.log('╚══════════════════════════════════════════════════════════╝');
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Graceful Shutdown
|
||||
// ==========================================
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Received SIGTERM, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Received SIGINT, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
375
src/services/mt4-client.ts
Normal file
375
src/services/mt4-client.ts
Normal file
@ -0,0 +1,375 @@
|
||||
/**
|
||||
* MT4 Client Service
|
||||
*
|
||||
* HTTP client wrapper for communicating with mt4-gateway.
|
||||
* This service mirrors the functionality of mt4_bridge_client.py
|
||||
* but is written in TypeScript for the MCP Server.
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
|
||||
// ==========================================
|
||||
// Types
|
||||
// ==========================================
|
||||
|
||||
export interface MT4AccountInfo {
|
||||
balance: number;
|
||||
equity: number;
|
||||
margin: number;
|
||||
freeMargin: number;
|
||||
marginLevel: number | null;
|
||||
profit: number;
|
||||
currency: string;
|
||||
leverage: number;
|
||||
name: string;
|
||||
server: string;
|
||||
company: string;
|
||||
}
|
||||
|
||||
export interface MT4Position {
|
||||
ticket: number;
|
||||
symbol: string;
|
||||
type: 'buy' | 'sell';
|
||||
lots: number;
|
||||
openPrice: number;
|
||||
currentPrice: number;
|
||||
stopLoss: number | null;
|
||||
takeProfit: number | null;
|
||||
profit: number;
|
||||
swap: number;
|
||||
openTime: string;
|
||||
magic: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export interface MT4Tick {
|
||||
symbol: string;
|
||||
bid: number;
|
||||
ask: number;
|
||||
timestamp: string;
|
||||
spread: number;
|
||||
}
|
||||
|
||||
export interface TradeResult {
|
||||
success: boolean;
|
||||
ticket?: number;
|
||||
message: string;
|
||||
errorCode?: number;
|
||||
}
|
||||
|
||||
export interface TradeRequest {
|
||||
action: 'buy' | 'sell';
|
||||
symbol: string;
|
||||
lots: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
price?: number;
|
||||
slippage?: number;
|
||||
magic?: number;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface ClosePositionRequest {
|
||||
ticket: number;
|
||||
lots?: number;
|
||||
slippage?: number;
|
||||
}
|
||||
|
||||
export interface ModifyPositionRequest {
|
||||
ticket: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
}
|
||||
|
||||
export interface MT4ClientConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
authToken: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MT4 Client Class
|
||||
// ==========================================
|
||||
|
||||
export class MT4Client {
|
||||
private client: AxiosInstance;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(config: MT4ClientConfig) {
|
||||
this.baseUrl = `http://${config.host}:${config.port}`;
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: this.baseUrl,
|
||||
timeout: config.timeout || 10000,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.authToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the MT4 terminal is connected
|
||||
*/
|
||||
async isConnected(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.client.get('/status');
|
||||
return response.data?.connected ?? false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MT4 account information
|
||||
*/
|
||||
async getAccountInfo(): Promise<MT4AccountInfo> {
|
||||
try {
|
||||
const response = await this.client.get('/account');
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
balance: data.balance ?? 0,
|
||||
equity: data.equity ?? 0,
|
||||
margin: data.margin ?? 0,
|
||||
freeMargin: data.freeMargin ?? 0,
|
||||
marginLevel: data.marginLevel ?? null,
|
||||
profit: data.profit ?? 0,
|
||||
currency: data.currency ?? 'USD',
|
||||
leverage: data.leverage ?? 100,
|
||||
name: data.name ?? '',
|
||||
server: data.server ?? '',
|
||||
company: data.company ?? '',
|
||||
};
|
||||
} catch (error) {
|
||||
throw this.handleError(error, 'Failed to get account info');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current tick (quote) for a symbol
|
||||
*/
|
||||
async getTick(symbol: string): Promise<MT4Tick> {
|
||||
try {
|
||||
const response = await this.client.get(`/tick/${symbol}`);
|
||||
const data = response.data;
|
||||
|
||||
const bid = data.bid ?? 0;
|
||||
const ask = data.ask ?? 0;
|
||||
|
||||
return {
|
||||
symbol,
|
||||
bid,
|
||||
ask,
|
||||
timestamp: data.time ?? new Date().toISOString(),
|
||||
spread: Math.round((ask - bid) * 100000) / 100000,
|
||||
};
|
||||
} catch (error) {
|
||||
throw this.handleError(error, `Failed to get tick for ${symbol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all open positions
|
||||
*/
|
||||
async getPositions(): Promise<MT4Position[]> {
|
||||
try {
|
||||
const response = await this.client.get('/positions');
|
||||
const data = response.data;
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.map((p: Record<string, unknown>) => ({
|
||||
ticket: (p.ticket as number) ?? 0,
|
||||
symbol: (p.symbol as string) ?? '',
|
||||
type: (p.type as 'buy' | 'sell') ?? 'buy',
|
||||
lots: (p.lots as number) ?? 0,
|
||||
openPrice: (p.openPrice as number) ?? 0,
|
||||
currentPrice: (p.currentPrice as number) ?? 0,
|
||||
stopLoss: (p.stopLoss as number | null) ?? null,
|
||||
takeProfit: (p.takeProfit as number | null) ?? null,
|
||||
profit: (p.profit as number) ?? 0,
|
||||
swap: (p.swap as number) ?? 0,
|
||||
openTime: (p.openTime as string) ?? new Date().toISOString(),
|
||||
magic: (p.magic as number) ?? 0,
|
||||
comment: (p.comment as string) ?? '',
|
||||
}));
|
||||
} catch (error) {
|
||||
throw this.handleError(error, 'Failed to get positions');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific position by ticket
|
||||
*/
|
||||
async getPosition(ticket: number): Promise<MT4Position | null> {
|
||||
const positions = await this.getPositions();
|
||||
return positions.find(p => p.ticket === ticket) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a trade (buy or sell)
|
||||
*/
|
||||
async executeTrade(request: TradeRequest): Promise<TradeResult> {
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
action: request.action,
|
||||
symbol: request.symbol,
|
||||
lots: request.lots,
|
||||
slippage: request.slippage ?? 3,
|
||||
magic: request.magic ?? 12345,
|
||||
comment: request.comment ?? 'MCP-MT4',
|
||||
};
|
||||
|
||||
if (request.stopLoss !== undefined) {
|
||||
payload.stopLoss = request.stopLoss;
|
||||
}
|
||||
if (request.takeProfit !== undefined) {
|
||||
payload.takeProfit = request.takeProfit;
|
||||
}
|
||||
if (request.price !== undefined) {
|
||||
payload.price = request.price;
|
||||
}
|
||||
|
||||
const response = await this.client.post('/trade', payload);
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
success: data.success ?? false,
|
||||
ticket: data.ticket,
|
||||
message: data.message ?? '',
|
||||
errorCode: data.errorCode,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: this.getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a position
|
||||
*/
|
||||
async closePosition(request: ClosePositionRequest): Promise<TradeResult> {
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
action: 'close',
|
||||
ticket: request.ticket,
|
||||
slippage: request.slippage ?? 3,
|
||||
};
|
||||
|
||||
if (request.lots !== undefined) {
|
||||
payload.lots = request.lots;
|
||||
}
|
||||
|
||||
const response = await this.client.post('/trade', payload);
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
success: data.success ?? false,
|
||||
ticket: request.ticket,
|
||||
message: data.message ?? '',
|
||||
errorCode: data.errorCode,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
ticket: request.ticket,
|
||||
message: this.getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a position (SL/TP)
|
||||
*/
|
||||
async modifyPosition(request: ModifyPositionRequest): Promise<TradeResult> {
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
action: 'modify',
|
||||
ticket: request.ticket,
|
||||
};
|
||||
|
||||
if (request.stopLoss !== undefined) {
|
||||
payload.stopLoss = request.stopLoss;
|
||||
}
|
||||
if (request.takeProfit !== undefined) {
|
||||
payload.takeProfit = request.takeProfit;
|
||||
}
|
||||
|
||||
const response = await this.client.post('/trade', payload);
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
success: data.success ?? false,
|
||||
ticket: request.ticket,
|
||||
message: data.message ?? '',
|
||||
errorCode: data.errorCode,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
ticket: request.ticket,
|
||||
message: this.getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle axios errors and convert to meaningful messages
|
||||
*/
|
||||
private handleError(error: unknown, context: string): Error {
|
||||
const message = this.getErrorMessage(error);
|
||||
return new Error(`${context}: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error message from various error types
|
||||
*/
|
||||
private getErrorMessage(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosError = error as AxiosError;
|
||||
if (axiosError.response) {
|
||||
return `HTTP ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data)}`;
|
||||
}
|
||||
if (axiosError.code === 'ECONNREFUSED') {
|
||||
return 'Connection refused - MT4 Gateway is not running';
|
||||
}
|
||||
if (axiosError.code === 'ETIMEDOUT') {
|
||||
return 'Connection timeout - MT4 Gateway is not responding';
|
||||
}
|
||||
return axiosError.message;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Singleton Instance
|
||||
// ==========================================
|
||||
|
||||
let clientInstance: MT4Client | null = null;
|
||||
|
||||
export function getMT4Client(): MT4Client {
|
||||
if (!clientInstance) {
|
||||
const config: MT4ClientConfig = {
|
||||
host: process.env.MT4_GATEWAY_HOST || 'localhost',
|
||||
port: parseInt(process.env.MT4_GATEWAY_PORT || '8081', 10),
|
||||
authToken: process.env.MT4_GATEWAY_AUTH_TOKEN || 'secret',
|
||||
timeout: parseInt(process.env.REQUEST_TIMEOUT || '10000', 10),
|
||||
};
|
||||
clientInstance = new MT4Client(config);
|
||||
}
|
||||
return clientInstance;
|
||||
}
|
||||
|
||||
export function resetMT4Client(): void {
|
||||
clientInstance = null;
|
||||
}
|
||||
143
src/tools/account.ts
Normal file
143
src/tools/account.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* mt4_get_account - Get MT4 account information
|
||||
*
|
||||
* @description Retrieves comprehensive account information from the connected MT4 terminal
|
||||
* including balance, equity, margin, leverage, and broker details.
|
||||
*
|
||||
* @returns Account information object with balance, equity, margin details
|
||||
*
|
||||
* @example
|
||||
* const result = await mt4_get_account({});
|
||||
* // Returns:
|
||||
* // {
|
||||
* // balance: 10000.00,
|
||||
* // equity: 10250.50,
|
||||
* // margin: 500.00,
|
||||
* // freeMargin: 9750.50,
|
||||
* // marginLevel: 2050.10,
|
||||
* // profit: 250.50,
|
||||
* // currency: "USD",
|
||||
* // leverage: 100,
|
||||
* // name: "Demo Account",
|
||||
* // server: "ICMarkets-Demo",
|
||||
* // company: "IC Markets"
|
||||
* // }
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getMT4Client, MT4AccountInfo } from '../services/mt4-client';
|
||||
|
||||
// ==========================================
|
||||
// Schema Definition
|
||||
// ==========================================
|
||||
|
||||
export const mt4GetAccountSchema = {
|
||||
name: 'mt4_get_account',
|
||||
description: 'Get MT4 account information including balance, equity, margin, and broker details',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {},
|
||||
required: [] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
// Input validation schema (no params required)
|
||||
export const Mt4GetAccountInputSchema = z.object({});
|
||||
|
||||
export type Mt4GetAccountInput = z.infer<typeof Mt4GetAccountInputSchema>;
|
||||
|
||||
// ==========================================
|
||||
// Tool Implementation
|
||||
// ==========================================
|
||||
|
||||
export interface Mt4GetAccountResult {
|
||||
success: boolean;
|
||||
data?: MT4AccountInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_get_account(
|
||||
_params: Mt4GetAccountInput
|
||||
): Promise<Mt4GetAccountResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection first
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Get account info
|
||||
const accountInfo = await client.getAccountInfo();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: accountInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MCP Tool Handler
|
||||
// ==========================================
|
||||
|
||||
export async function handleMt4GetAccount(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
// Validate input
|
||||
const validatedParams = Mt4GetAccountInputSchema.parse(params);
|
||||
|
||||
// Execute tool
|
||||
const result = await mt4_get_account(validatedParams);
|
||||
|
||||
// Format response for MCP
|
||||
if (result.success && result.data) {
|
||||
const formattedOutput = `
|
||||
MT4 Account Information
|
||||
=======================
|
||||
Account Name: ${result.data.name}
|
||||
Server: ${result.data.server}
|
||||
Broker: ${result.data.company}
|
||||
Leverage: 1:${result.data.leverage}
|
||||
|
||||
Financial Summary
|
||||
-----------------
|
||||
Balance: ${result.data.balance.toFixed(2)} ${result.data.currency}
|
||||
Equity: ${result.data.equity.toFixed(2)} ${result.data.currency}
|
||||
Profit/Loss: ${result.data.profit >= 0 ? '+' : ''}${result.data.profit.toFixed(2)} ${result.data.currency}
|
||||
|
||||
Margin Details
|
||||
--------------
|
||||
Used Margin: ${result.data.margin.toFixed(2)} ${result.data.currency}
|
||||
Free Margin: ${result.data.freeMargin.toFixed(2)} ${result.data.currency}
|
||||
Margin Level: ${result.data.marginLevel !== null ? result.data.marginLevel.toFixed(2) + '%' : 'N/A'}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
212
src/tools/index.ts
Normal file
212
src/tools/index.ts
Normal file
@ -0,0 +1,212 @@
|
||||
/**
|
||||
* MCP Tools Index
|
||||
*
|
||||
* Exports all MT4 MCP tools and their schemas for registration
|
||||
*/
|
||||
|
||||
// Import handlers for use in toolHandlers map
|
||||
import { handleMt4GetAccount } from './account';
|
||||
import { handleMt4GetPositions, handleMt4ClosePosition } from './positions';
|
||||
import { handleMt4ExecuteTrade, handleMt4ModifyPosition } from './trading';
|
||||
import { handleMt4GetQuote } from './quotes';
|
||||
|
||||
// Account tools
|
||||
export {
|
||||
mt4GetAccountSchema,
|
||||
mt4_get_account,
|
||||
handleMt4GetAccount,
|
||||
Mt4GetAccountInputSchema,
|
||||
type Mt4GetAccountInput,
|
||||
type Mt4GetAccountResult,
|
||||
} from './account';
|
||||
|
||||
// Position tools
|
||||
export {
|
||||
mt4GetPositionsSchema,
|
||||
mt4_get_positions,
|
||||
handleMt4GetPositions,
|
||||
Mt4GetPositionsInputSchema,
|
||||
mt4ClosePositionSchema,
|
||||
mt4_close_position,
|
||||
handleMt4ClosePosition,
|
||||
Mt4ClosePositionInputSchema,
|
||||
type Mt4GetPositionsInput,
|
||||
type Mt4GetPositionsResult,
|
||||
type Mt4ClosePositionInput,
|
||||
type Mt4ClosePositionResult,
|
||||
} from './positions';
|
||||
|
||||
// Trading tools
|
||||
export {
|
||||
mt4ExecuteTradeSchema,
|
||||
mt4_execute_trade,
|
||||
handleMt4ExecuteTrade,
|
||||
Mt4ExecuteTradeInputSchema,
|
||||
mt4ModifyPositionSchema,
|
||||
mt4_modify_position,
|
||||
handleMt4ModifyPosition,
|
||||
Mt4ModifyPositionInputSchema,
|
||||
type Mt4ExecuteTradeInput,
|
||||
type Mt4ExecuteTradeResult,
|
||||
type Mt4ModifyPositionInput,
|
||||
type Mt4ModifyPositionResult,
|
||||
} from './trading';
|
||||
|
||||
// Quote tools
|
||||
export {
|
||||
mt4GetQuoteSchema,
|
||||
mt4_get_quote,
|
||||
handleMt4GetQuote,
|
||||
Mt4GetQuoteInputSchema,
|
||||
type Mt4GetQuoteInput,
|
||||
type Mt4GetQuoteResult,
|
||||
} from './quotes';
|
||||
|
||||
// ==========================================
|
||||
// Tool Registry
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* All available MCP tools with their schemas
|
||||
*/
|
||||
export const mcpToolSchemas = [
|
||||
{
|
||||
name: 'mt4_get_account',
|
||||
description: 'Get MT4 account information including balance, equity, margin, and broker details',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {},
|
||||
required: [] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_get_positions',
|
||||
description: 'List all open trading positions from MT4. Optionally filter by symbol.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Optional: Filter positions by symbol (e.g., XAUUSD, EURUSD)',
|
||||
},
|
||||
},
|
||||
required: [] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_execute_trade',
|
||||
description: 'Execute a market order (BUY or SELL) on MT4 with optional stop loss and take profit',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading symbol (e.g., XAUUSD, EURUSD, GBPUSD)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['buy', 'sell'],
|
||||
description: 'Trade direction: buy or sell',
|
||||
},
|
||||
lots: {
|
||||
type: 'number',
|
||||
description: 'Volume in lots (e.g., 0.01, 0.1, 1.0)',
|
||||
},
|
||||
stopLoss: {
|
||||
type: 'number',
|
||||
description: 'Optional: Stop loss price level',
|
||||
},
|
||||
takeProfit: {
|
||||
type: 'number',
|
||||
description: 'Optional: Take profit price level',
|
||||
},
|
||||
slippage: {
|
||||
type: 'number',
|
||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
||||
},
|
||||
magic: {
|
||||
type: 'number',
|
||||
description: 'Optional: Magic number for EA identification (default: 12345)',
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
description: 'Optional: Order comment (max 31 chars)',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'action', 'lots'] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_close_position',
|
||||
description: 'Close an open trading position by ticket number. Can close partially.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
ticket: {
|
||||
type: 'number',
|
||||
description: 'Position ticket number to close',
|
||||
},
|
||||
lots: {
|
||||
type: 'number',
|
||||
description: 'Optional: Partial volume to close. If not specified, closes entire position.',
|
||||
},
|
||||
slippage: {
|
||||
type: 'number',
|
||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
||||
},
|
||||
},
|
||||
required: ['ticket'] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_modify_position',
|
||||
description: 'Modify stop loss and/or take profit of an existing position',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
ticket: {
|
||||
type: 'number',
|
||||
description: 'Position ticket number to modify',
|
||||
},
|
||||
stopLoss: {
|
||||
type: 'number',
|
||||
description: 'New stop loss price level (optional)',
|
||||
},
|
||||
takeProfit: {
|
||||
type: 'number',
|
||||
description: 'New take profit price level (optional)',
|
||||
},
|
||||
},
|
||||
required: ['ticket'] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_get_quote',
|
||||
description: 'Get current price quote (bid/ask/spread) for a trading symbol',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading symbol to get quote for (e.g., XAUUSD, EURUSD, GBPUSD)',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Tool handler routing map
|
||||
*/
|
||||
export const toolHandlers: Record<
|
||||
string,
|
||||
(params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>
|
||||
> = {
|
||||
mt4_get_account: handleMt4GetAccount,
|
||||
mt4_get_positions: handleMt4GetPositions,
|
||||
mt4_execute_trade: handleMt4ExecuteTrade,
|
||||
mt4_close_position: handleMt4ClosePosition,
|
||||
mt4_modify_position: handleMt4ModifyPosition,
|
||||
mt4_get_quote: handleMt4GetQuote,
|
||||
};
|
||||
315
src/tools/positions.ts
Normal file
315
src/tools/positions.ts
Normal file
@ -0,0 +1,315 @@
|
||||
/**
|
||||
* MT4 Position Tools
|
||||
*
|
||||
* - mt4_get_positions: List all open positions
|
||||
* - mt4_close_position: Close a specific position
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getMT4Client, MT4Position, TradeResult } from '../services/mt4-client';
|
||||
|
||||
// ==========================================
|
||||
// mt4_get_positions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* mt4_get_positions - List all open positions
|
||||
*
|
||||
* @description Retrieves all currently open positions from MT4 terminal.
|
||||
* Can optionally filter by symbol.
|
||||
*
|
||||
* @param symbol - Optional symbol to filter positions (e.g., "XAUUSD")
|
||||
* @returns Array of open positions with details
|
||||
*
|
||||
* @example
|
||||
* const result = await mt4_get_positions({});
|
||||
* // Returns all positions
|
||||
*
|
||||
* const result = await mt4_get_positions({ symbol: "XAUUSD" });
|
||||
* // Returns only XAUUSD positions
|
||||
*/
|
||||
|
||||
export const mt4GetPositionsSchema = {
|
||||
name: 'mt4_get_positions',
|
||||
description: 'List all open trading positions from MT4. Optionally filter by symbol.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Optional: Filter positions by symbol (e.g., XAUUSD, EURUSD)',
|
||||
},
|
||||
},
|
||||
required: [] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4GetPositionsInputSchema = z.object({
|
||||
symbol: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Mt4GetPositionsInput = z.infer<typeof Mt4GetPositionsInputSchema>;
|
||||
|
||||
export interface Mt4GetPositionsResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
positions: MT4Position[];
|
||||
totalProfit: number;
|
||||
count: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_get_positions(
|
||||
params: Mt4GetPositionsInput
|
||||
): Promise<Mt4GetPositionsResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Get all positions
|
||||
let positions = await client.getPositions();
|
||||
|
||||
// Filter by symbol if specified
|
||||
if (params.symbol) {
|
||||
positions = positions.filter(
|
||||
p => p.symbol.toUpperCase() === params.symbol!.toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate total profit
|
||||
const totalProfit = positions.reduce((sum, p) => sum + p.profit, 0);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
positions,
|
||||
totalProfit,
|
||||
count: positions.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMt4GetPositions(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4GetPositionsInputSchema.parse(params);
|
||||
const result = await mt4_get_positions(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
if (result.data.count === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: params && (params as Mt4GetPositionsInput).symbol
|
||||
? `No open positions found for ${(params as Mt4GetPositionsInput).symbol}`
|
||||
: 'No open positions found',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const positionLines = result.data.positions.map(p => {
|
||||
const direction = p.type.toUpperCase();
|
||||
const profitSign = p.profit >= 0 ? '+' : '';
|
||||
const slInfo = p.stopLoss !== null ? `SL: ${p.stopLoss}` : 'SL: None';
|
||||
const tpInfo = p.takeProfit !== null ? `TP: ${p.takeProfit}` : 'TP: None';
|
||||
|
||||
return `
|
||||
#${p.ticket} | ${p.symbol} | ${direction} ${p.lots} lots
|
||||
Open: ${p.openPrice} | Current: ${p.currentPrice}
|
||||
${slInfo} | ${tpInfo}
|
||||
P/L: ${profitSign}${p.profit.toFixed(2)} | Swap: ${p.swap.toFixed(2)}
|
||||
Opened: ${p.openTime}
|
||||
Magic: ${p.magic} | Comment: ${p.comment || 'None'}`;
|
||||
});
|
||||
|
||||
const formattedOutput = `
|
||||
Open Positions (${result.data.count})
|
||||
${'='.repeat(30)}
|
||||
${positionLines.join('\n---\n')}
|
||||
|
||||
Total P/L: ${result.data.totalProfit >= 0 ? '+' : ''}${result.data.totalProfit.toFixed(2)}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// mt4_close_position
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* mt4_close_position - Close a trading position
|
||||
*
|
||||
* @description Closes an open position by ticket number.
|
||||
* Can optionally close partial volume.
|
||||
*
|
||||
* @param ticket - Position ticket number to close
|
||||
* @param lots - Optional: Partial volume to close (default: close all)
|
||||
* @param slippage - Optional: Maximum slippage in points (default: 3)
|
||||
* @returns Trade result with success status
|
||||
*
|
||||
* @example
|
||||
* // Close entire position
|
||||
* const result = await mt4_close_position({ ticket: 123456 });
|
||||
*
|
||||
* // Close partial position
|
||||
* const result = await mt4_close_position({ ticket: 123456, lots: 0.5 });
|
||||
*/
|
||||
|
||||
export const mt4ClosePositionSchema = {
|
||||
name: 'mt4_close_position',
|
||||
description: 'Close an open trading position by ticket number. Can close partially.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
ticket: {
|
||||
type: 'number',
|
||||
description: 'Position ticket number to close',
|
||||
},
|
||||
lots: {
|
||||
type: 'number',
|
||||
description: 'Optional: Partial volume to close. If not specified, closes entire position.',
|
||||
},
|
||||
slippage: {
|
||||
type: 'number',
|
||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
||||
},
|
||||
},
|
||||
required: ['ticket'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4ClosePositionInputSchema = z.object({
|
||||
ticket: z.number().int().positive(),
|
||||
lots: z.number().positive().optional(),
|
||||
slippage: z.number().int().min(0).max(100).optional(),
|
||||
});
|
||||
|
||||
export type Mt4ClosePositionInput = z.infer<typeof Mt4ClosePositionInputSchema>;
|
||||
|
||||
export interface Mt4ClosePositionResult {
|
||||
success: boolean;
|
||||
data?: TradeResult;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_close_position(
|
||||
params: Mt4ClosePositionInput
|
||||
): Promise<Mt4ClosePositionResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Verify position exists
|
||||
const position = await client.getPosition(params.ticket);
|
||||
if (!position) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Position with ticket ${params.ticket} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate lots if specified
|
||||
if (params.lots !== undefined && params.lots > position.lots) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Requested lots (${params.lots}) exceeds position size (${position.lots})`,
|
||||
};
|
||||
}
|
||||
|
||||
// Close position
|
||||
const result = await client.closePosition({
|
||||
ticket: params.ticket,
|
||||
lots: params.lots,
|
||||
slippage: params.slippage,
|
||||
});
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
data: result,
|
||||
error: result.success ? undefined : result.message,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMt4ClosePosition(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4ClosePositionInputSchema.parse(params);
|
||||
const result = await mt4_close_position(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const formattedOutput = `
|
||||
Position Closed Successfully
|
||||
============================
|
||||
Ticket: ${validatedParams.ticket}
|
||||
${validatedParams.lots ? `Closed Volume: ${validatedParams.lots} lots` : 'Closed: Entire position'}
|
||||
Message: ${result.data.message || 'Position closed'}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error closing position: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
193
src/tools/quotes.ts
Normal file
193
src/tools/quotes.ts
Normal file
@ -0,0 +1,193 @@
|
||||
/**
|
||||
* mt4_get_quote - Get current price quote for a symbol
|
||||
*
|
||||
* @description Retrieves the current bid/ask prices for a trading symbol.
|
||||
* Also calculates the spread in points.
|
||||
*
|
||||
* @param symbol - Trading symbol to get quote for (e.g., "XAUUSD")
|
||||
* @returns Current bid, ask, spread, and timestamp
|
||||
*
|
||||
* @example
|
||||
* const result = await mt4_get_quote({ symbol: "XAUUSD" });
|
||||
* // Returns:
|
||||
* // {
|
||||
* // symbol: "XAUUSD",
|
||||
* // bid: 2650.50,
|
||||
* // ask: 2650.80,
|
||||
* // spread: 0.30,
|
||||
* // timestamp: "2026-01-04T12:00:00.000Z"
|
||||
* // }
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getMT4Client, MT4Tick } from '../services/mt4-client';
|
||||
|
||||
// ==========================================
|
||||
// Schema Definition
|
||||
// ==========================================
|
||||
|
||||
export const mt4GetQuoteSchema = {
|
||||
name: 'mt4_get_quote',
|
||||
description: 'Get current price quote (bid/ask/spread) for a trading symbol',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading symbol to get quote for (e.g., XAUUSD, EURUSD, GBPUSD)',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4GetQuoteInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20),
|
||||
});
|
||||
|
||||
export type Mt4GetQuoteInput = z.infer<typeof Mt4GetQuoteInputSchema>;
|
||||
|
||||
// ==========================================
|
||||
// Tool Implementation
|
||||
// ==========================================
|
||||
|
||||
export interface Mt4GetQuoteResult {
|
||||
success: boolean;
|
||||
data?: MT4Tick;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_get_quote(
|
||||
params: Mt4GetQuoteInput
|
||||
): Promise<Mt4GetQuoteResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Get tick data
|
||||
const tick = await client.getTick(params.symbol.toUpperCase());
|
||||
|
||||
// Validate we got valid data
|
||||
if (tick.bid === 0 && tick.ask === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No quote data available for ${params.symbol}. Symbol may not be available on this broker.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: tick,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MCP Tool Handler
|
||||
// ==========================================
|
||||
|
||||
export async function handleMt4GetQuote(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4GetQuoteInputSchema.parse(params);
|
||||
const result = await mt4_get_quote(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
// Determine decimal places based on symbol
|
||||
const decimals = getDecimalPlaces(result.data.symbol);
|
||||
const spreadPips = calculateSpreadPips(result.data.spread, result.data.symbol);
|
||||
|
||||
const formattedOutput = `
|
||||
Price Quote: ${result.data.symbol}
|
||||
${'='.repeat(25)}
|
||||
Bid: ${result.data.bid.toFixed(decimals)}
|
||||
Ask: ${result.data.ask.toFixed(decimals)}
|
||||
Spread: ${result.data.spread.toFixed(decimals)} (${spreadPips.toFixed(1)} pips)
|
||||
Time: ${result.data.timestamp}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Helper Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Determine decimal places based on symbol type
|
||||
*/
|
||||
function getDecimalPlaces(symbol: string): number {
|
||||
const upperSymbol = symbol.toUpperCase();
|
||||
|
||||
// JPY pairs have 3 decimals, most forex 5 decimals
|
||||
if (upperSymbol.includes('JPY')) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
// Gold and metals typically 2 decimals
|
||||
if (upperSymbol.startsWith('XAU') || upperSymbol.startsWith('XAG')) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Indices vary, use 2 as default
|
||||
if (
|
||||
upperSymbol.includes('US30') ||
|
||||
upperSymbol.includes('US500') ||
|
||||
upperSymbol.includes('NAS')
|
||||
) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Default forex pairs
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate spread in pips based on symbol
|
||||
*/
|
||||
function calculateSpreadPips(spread: number, symbol: string): number {
|
||||
const upperSymbol = symbol.toUpperCase();
|
||||
|
||||
// JPY pairs: 1 pip = 0.01
|
||||
if (upperSymbol.includes('JPY')) {
|
||||
return spread * 100;
|
||||
}
|
||||
|
||||
// Gold: 1 pip = 0.10
|
||||
if (upperSymbol.startsWith('XAU')) {
|
||||
return spread * 10;
|
||||
}
|
||||
|
||||
// Default forex: 1 pip = 0.0001
|
||||
return spread * 10000;
|
||||
}
|
||||
402
src/tools/trading.ts
Normal file
402
src/tools/trading.ts
Normal file
@ -0,0 +1,402 @@
|
||||
/**
|
||||
* MT4 Trading Tools
|
||||
*
|
||||
* - mt4_execute_trade: Execute BUY/SELL market orders
|
||||
* - mt4_modify_position: Modify SL/TP of existing positions
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getMT4Client, TradeResult } from '../services/mt4-client';
|
||||
|
||||
// ==========================================
|
||||
// mt4_execute_trade
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* mt4_execute_trade - Execute a market order (BUY or SELL)
|
||||
*
|
||||
* @description Opens a new trading position with optional SL/TP levels.
|
||||
* Supports market orders only (pending orders not implemented).
|
||||
*
|
||||
* @param symbol - Trading symbol (e.g., "XAUUSD", "EURUSD")
|
||||
* @param action - Trade direction: "buy" or "sell"
|
||||
* @param lots - Volume in lots (e.g., 0.01, 0.1, 1.0)
|
||||
* @param stopLoss - Optional stop loss price
|
||||
* @param takeProfit - Optional take profit price
|
||||
* @param slippage - Optional max slippage in points (default: 3)
|
||||
* @param magic - Optional magic number for EA identification
|
||||
* @param comment - Optional order comment
|
||||
* @returns Trade result with ticket number
|
||||
*
|
||||
* @example
|
||||
* // Simple buy order
|
||||
* const result = await mt4_execute_trade({
|
||||
* symbol: "XAUUSD",
|
||||
* action: "buy",
|
||||
* lots: 0.1
|
||||
* });
|
||||
*
|
||||
* // Buy with SL/TP
|
||||
* const result = await mt4_execute_trade({
|
||||
* symbol: "XAUUSD",
|
||||
* action: "buy",
|
||||
* lots: 0.1,
|
||||
* stopLoss: 2640.00,
|
||||
* takeProfit: 2680.00,
|
||||
* comment: "AI Signal"
|
||||
* });
|
||||
*/
|
||||
|
||||
export const mt4ExecuteTradeSchema = {
|
||||
name: 'mt4_execute_trade',
|
||||
description: 'Execute a market order (BUY or SELL) on MT4 with optional stop loss and take profit',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading symbol (e.g., XAUUSD, EURUSD, GBPUSD)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['buy', 'sell'],
|
||||
description: 'Trade direction: buy or sell',
|
||||
},
|
||||
lots: {
|
||||
type: 'number',
|
||||
description: 'Volume in lots (e.g., 0.01, 0.1, 1.0)',
|
||||
},
|
||||
stopLoss: {
|
||||
type: 'number',
|
||||
description: 'Optional: Stop loss price level',
|
||||
},
|
||||
takeProfit: {
|
||||
type: 'number',
|
||||
description: 'Optional: Take profit price level',
|
||||
},
|
||||
slippage: {
|
||||
type: 'number',
|
||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
||||
},
|
||||
magic: {
|
||||
type: 'number',
|
||||
description: 'Optional: Magic number for EA identification (default: 12345)',
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
description: 'Optional: Order comment (max 31 chars)',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'action', 'lots'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4ExecuteTradeInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20),
|
||||
action: z.enum(['buy', 'sell']),
|
||||
lots: z.number().positive().max(100),
|
||||
stopLoss: z.number().positive().optional(),
|
||||
takeProfit: z.number().positive().optional(),
|
||||
slippage: z.number().int().min(0).max(100).optional(),
|
||||
magic: z.number().int().optional(),
|
||||
comment: z.string().max(31).optional(),
|
||||
});
|
||||
|
||||
export type Mt4ExecuteTradeInput = z.infer<typeof Mt4ExecuteTradeInputSchema>;
|
||||
|
||||
export interface Mt4ExecuteTradeResult {
|
||||
success: boolean;
|
||||
data?: TradeResult & {
|
||||
symbol: string;
|
||||
action: string;
|
||||
lots: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_execute_trade(
|
||||
params: Mt4ExecuteTradeInput
|
||||
): Promise<Mt4ExecuteTradeResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Validate SL/TP logic (basic check)
|
||||
if (params.stopLoss !== undefined && params.takeProfit !== undefined) {
|
||||
if (params.action === 'buy') {
|
||||
// For buy: SL should be below current price, TP above
|
||||
if (params.stopLoss >= params.takeProfit) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'For BUY orders, stop loss must be below take profit',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// For sell: SL should be above current price, TP below
|
||||
if (params.stopLoss <= params.takeProfit) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'For SELL orders, stop loss must be above take profit',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute trade
|
||||
const result = await client.executeTrade({
|
||||
symbol: params.symbol.toUpperCase(),
|
||||
action: params.action,
|
||||
lots: params.lots,
|
||||
stopLoss: params.stopLoss,
|
||||
takeProfit: params.takeProfit,
|
||||
slippage: params.slippage,
|
||||
magic: params.magic,
|
||||
comment: params.comment,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...result,
|
||||
symbol: params.symbol.toUpperCase(),
|
||||
action: params.action,
|
||||
lots: params.lots,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || 'Trade execution failed',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMt4ExecuteTrade(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4ExecuteTradeInputSchema.parse(params);
|
||||
const result = await mt4_execute_trade(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const formattedOutput = `
|
||||
Trade Executed Successfully
|
||||
===========================
|
||||
Ticket: #${result.data.ticket}
|
||||
Symbol: ${result.data.symbol}
|
||||
Direction: ${result.data.action.toUpperCase()}
|
||||
Volume: ${result.data.lots} lots
|
||||
${validatedParams.stopLoss ? `Stop Loss: ${validatedParams.stopLoss}` : 'Stop Loss: Not set'}
|
||||
${validatedParams.takeProfit ? `Take Profit: ${validatedParams.takeProfit}` : 'Take Profit: Not set'}
|
||||
Message: ${result.data.message || 'Order placed successfully'}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error executing trade: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// mt4_modify_position
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* mt4_modify_position - Modify SL/TP of an existing position
|
||||
*
|
||||
* @description Updates the stop loss and/or take profit levels of an open position.
|
||||
*
|
||||
* @param ticket - Position ticket number to modify
|
||||
* @param stopLoss - New stop loss price (null to remove)
|
||||
* @param takeProfit - New take profit price (null to remove)
|
||||
* @returns Modification result
|
||||
*
|
||||
* @example
|
||||
* // Set both SL and TP
|
||||
* const result = await mt4_modify_position({
|
||||
* ticket: 123456,
|
||||
* stopLoss: 2640.00,
|
||||
* takeProfit: 2680.00
|
||||
* });
|
||||
*
|
||||
* // Update only TP
|
||||
* const result = await mt4_modify_position({
|
||||
* ticket: 123456,
|
||||
* takeProfit: 2700.00
|
||||
* });
|
||||
*/
|
||||
|
||||
export const mt4ModifyPositionSchema = {
|
||||
name: 'mt4_modify_position',
|
||||
description: 'Modify stop loss and/or take profit of an existing position',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
ticket: {
|
||||
type: 'number',
|
||||
description: 'Position ticket number to modify',
|
||||
},
|
||||
stopLoss: {
|
||||
type: 'number',
|
||||
description: 'New stop loss price level (optional)',
|
||||
},
|
||||
takeProfit: {
|
||||
type: 'number',
|
||||
description: 'New take profit price level (optional)',
|
||||
},
|
||||
},
|
||||
required: ['ticket'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4ModifyPositionInputSchema = z.object({
|
||||
ticket: z.number().int().positive(),
|
||||
stopLoss: z.number().positive().optional(),
|
||||
takeProfit: z.number().positive().optional(),
|
||||
});
|
||||
|
||||
export type Mt4ModifyPositionInput = z.infer<typeof Mt4ModifyPositionInputSchema>;
|
||||
|
||||
export interface Mt4ModifyPositionResult {
|
||||
success: boolean;
|
||||
data?: TradeResult;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_modify_position(
|
||||
params: Mt4ModifyPositionInput
|
||||
): Promise<Mt4ModifyPositionResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Validate at least one parameter is provided
|
||||
if (params.stopLoss === undefined && params.takeProfit === undefined) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'At least one of stopLoss or takeProfit must be provided',
|
||||
};
|
||||
}
|
||||
|
||||
// Verify position exists
|
||||
const position = await client.getPosition(params.ticket);
|
||||
if (!position) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Position with ticket ${params.ticket} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate SL/TP based on position type
|
||||
const sl = params.stopLoss;
|
||||
const tp = params.takeProfit;
|
||||
|
||||
if (sl !== undefined && tp !== undefined) {
|
||||
if (position.type === 'buy') {
|
||||
if (sl >= tp) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'For BUY positions, stop loss must be below take profit',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (sl <= tp) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'For SELL positions, stop loss must be above take profit',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modify position
|
||||
const result = await client.modifyPosition({
|
||||
ticket: params.ticket,
|
||||
stopLoss: params.stopLoss,
|
||||
takeProfit: params.takeProfit,
|
||||
});
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
data: result,
|
||||
error: result.success ? undefined : result.message,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMt4ModifyPosition(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4ModifyPositionInputSchema.parse(params);
|
||||
const result = await mt4_modify_position(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const formattedOutput = `
|
||||
Position Modified Successfully
|
||||
==============================
|
||||
Ticket: #${validatedParams.ticket}
|
||||
${validatedParams.stopLoss !== undefined ? `New Stop Loss: ${validatedParams.stopLoss}` : 'Stop Loss: Unchanged'}
|
||||
${validatedParams.takeProfit !== undefined ? `New Take Profit: ${validatedParams.takeProfit}` : 'Take Profit: Unchanged'}
|
||||
Message: ${result.data.message || 'Position modified successfully'}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error modifying position: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user