From 061cac2d5adad1b42bfdad80c4b6964d4ab8c7ba Mon Sep 17 00:00:00 2001 From: yjkt Date: Sun, 17 May 2026 11:26:45 +0800 Subject: [PATCH] v0.1 --- .gitignore | 1 + Cargo.lock | 230 ++++++++ Cargo.toml | 31 ++ LICENSE | 21 + README.md | 135 +++++ README_CN.md | 135 +++++ src/client.rs | 1345 +++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 27 + src/lib.rs | 9 + src/protocol.rs | 288 ++++++++++ src/types.rs | 269 ++++++++++ 11 files changed, 2491 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README_CN.md create mode 100644 src/client.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/protocol.rs create mode 100644 src/types.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5c398bc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,230 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "mt5-rs" +version = "0.1.0" +dependencies = [ + "hex", + "sha2", + "thiserror", + "windows-sys", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cf6b05d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "mt5-rs" +version = "0.1.0" +edition = "2021" +rust-version = "1.70" +description = "A pure Rust library for MetaTrader 5 IPC communication (no Python dependency)" +license = "MIT" +repository = "https://github.com/yjkt/mt5-rs" +documentation = "https://github.com/yjkt/mt5-rs" +readme = "README.md" +keywords = ["metatrader", "mt5", "trading", "forex", "ipc"] +categories = ["api-bindings", "finance"] +authors = ["yjkt "] + +[dependencies] +windows-sys = { version = "0.59", features = [ + "Win32_System_Pipes", + "Win32_Storage_FileSystem", + "Win32_Foundation", + "Win32_Security", + "Win32_System_IO", + "Win32_System_Threading", + "Win32_System_Diagnostics_ToolHelp", +] } +thiserror = "1.0" +sha2 = "0.10" +hex = "0.4" + +[lib] +name = "mt5_rs" +path = "src/lib.rs" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..452291b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 yjkt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b94fbe6 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# mt5-rs + +A pure Rust library for MetaTrader 5 IPC communication. No Python dependency. + +Compatible with Python `MetaTrader5` library API. + +## Features + +- Pure Rust implementation, no Python or C++ dependency +- Windows named pipe IPC communication with MT5 terminal +- Full compatibility with Python `MetaTrader5` library (30/32 functions) +- Support MT5 Build 5836+ + +## Quick Start + +```rust +use mt5_rs::{Mt5Client, discover_mt5_pipe}; + +fn main() -> Result<(), Box> { + // Auto-discover MT5 pipe + let pipe_name = discover_mt5_pipe(); + + // Initialize client + let mut client = Mt5Client::new(); + client.initialize(Some(&pipe_name))?; + + // Get account info + let account = client.account_info()?; + println!("Balance: {}", account.balance); + println!("Equity: {}", account.equity); + println!("Margin Free: {}", account.margin_free); + + Ok(()) +} +``` + +## API Reference + +### Initialization + +| Function | Description | +|----------|-------------| +| `Mt5Client::new()` | Create a new client | +| `initialize(pipe_name)` | Initialize connection to MT5 | +| `shutdown()` | Close connection | +| `login(login, password, server)` | Login to MT5 account | + +### Account & Terminal + +| Function | Description | +|----------|-------------| +| `account_info()` | Get account information | +| `terminal_info()` | Get terminal information | +| `version()` | Get MT5 version | +| `last_error()` | Get last error code and message | + +### Symbols + +| Function | Description | +|----------|-------------| +| `symbol_info(symbol)` | Get symbol information | +| `symbol_info_tick(symbol)` | Get current tick for symbol | +| `symbols_get()` | Get all available symbols | +| `symbol_select(symbol, enable)` | Select/deselect symbol in Market Watch | + +### Market Data + +| Function | Description | +|----------|-------------| +| `copy_rates_from_pos(symbol, timeframe, start_pos, count)` | Copy bars from position | +| `copy_rates_from(symbol, timeframe, date_from, count)` | Copy bars from date | +| `copy_rates_range(symbol, timeframe, date_from, date_to)` | Copy bars in range | +| `copy_ticks_from(symbol, from, count, flags)` | Copy ticks from time | +| `copy_ticks_range(symbol, from, to, flags)` | Copy ticks in range | + +### Positions & Orders + +| Function | Description | +|----------|-------------| +| `positions_total()` | Get total number of open positions | +| `positions_get(symbol)` | Get open positions | +| `orders_total()` | Get total number of pending orders | +| `orders_get(symbol)` | Get pending orders | + +### History + +| Function | Description | +|----------|-------------| +| `history_deals_total(from, to)` | Get total number of deals in range | +| `history_deals_get(from, to)` | Get deals in range | +| `history_orders_total(from, to)` | Get total number of orders in range | +| `history_orders_get(from, to)` | Get orders in range | + +### Market Depth + +| Function | Description | +|----------|-------------| +| `market_book_add(symbol)` | Subscribe to market depth | +| `market_book_get(symbol)` | Get market depth data | +| `market_book_release(symbol)` | Unsubscribe from market depth | + +### Trading Calculations + +| Function | Description | +|----------|-------------| +| `order_calc_margin(action, symbol, volume, price)` | Calculate required margin (local calculation) | +| `order_calc_profit(action, symbol, volume, price_open, price_close)` | Calculate expected profit (local calculation) | + +### Not Implemented + +| Function | Status | +|----------|--------| +| `order_check(request)` | TODO - Check trade request validity | +| `order_send(request)` | TODO - Send trade request to MT5 | + +## Implementation Notes + +### Local Calculation for `order_calc_margin` and `order_calc_profit` + +Unlike `go-mt5` which sends commands 202/203 via named pipe, this library uses **local calculation** (same as Python `MetaTrader5` library): + +- `order_calc_margin`: `margin = volume × price × margin_initial / 4` +- `order_calc_profit`: `profit = volume × (price_close - price_open) × trade_contract_size` + +This approach avoids the "pipe closed" error that occurs with MT5 Build 5836+ when using IPC for these commands. + +## Requirements + +- Rust 2021 edition +- Windows OS (named pipe IPC is Windows-specific) +- MT5 terminal must be running + +## License + +MIT diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..3b16e05 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,135 @@ +# mt5-rs + +纯 Rust 实现的 MetaTrader 5 IPC 通信库,无需 Python 依赖。 + +与 Python `MetaTrader5` 库 API 完全兼容。 + +## 特性 + +- 纯 Rust 实现,无 Python 或 C++ 依赖 +- 通过 Windows 命名管道与 MT5 终端进行 IPC 通信 +- 与 Python `MetaTrader5` 库完全兼容(32个函数中的30个) +- 支持 MT5 Build 5836+ + +## 快速开始 + +```rust +use mt5_rs::{Mt5Client, discover_mt5_pipe}; + +fn main() -> Result<(), Box> { + // 自动发现 MT5 管道 + let pipe_name = discover_mt5_pipe(); + + // 初始化客户端 + let mut client = Mt5Client::new(); + client.initialize(Some(&pipe_name))?; + + // 获取账户信息 + let account = client.account_info()?; + println!("Balance: {}", account.balance); + println!("Equity: {}", account.equity); + println!("Margin Free: {}", account.margin_free); + + Ok(()) +} +``` + +## API 参考 + +### 初始化 + +| 函数 | 描述 | +|------|------| +| `Mt5Client::new()` | 创建新客户端 | +| `initialize(pipe_name)` | 初始化与 MT5 的连接 | +| `shutdown()` | 关闭连接 | +| `login(login, password, server)` | 登录 MT5 账户 | + +### 账户与终端 + +| 函数 | 描述 | +|------|------| +| `account_info()` | 获取账户信息 | +| `terminal_info()` | 获取终端信息 | +| `version()` | 获取 MT5 版本 | +| `last_error()` | 获取最后错误代码和消息 | + +### 交易品种 + +| 函数 | 描述 | +|------|------| +| `symbol_info(symbol)` | 获取品种信息 | +| `symbol_info_tick(symbol)` | 获取品种当前报价 | +| `symbols_get()` | 获取所有可用品种 | +| `symbol_select(symbol, enable)` | 在市场报价中选择/取消选择品种 | + +### 行情数据 + +| 函数 | 描述 | +|------|------| +| `copy_rates_from_pos(symbol, timeframe, start_pos, count)` | 从指定位置复制K线 | +| `copy_rates_from(symbol, timeframe, date_from, count)` | 从指定日期复制K线 | +| `copy_rates_range(symbol, timeframe, date_from, date_to)` | 复制指定范围的K线 | +| `copy_ticks_from(symbol, from, count, flags)` | 从指定时间复制Tick | +| `copy_ticks_range(symbol, from, to, flags)` | 复制指定范围的Tick | + +### 持仓与订单 + +| 函数 | 描述 | +|------|------| +| `positions_total()` | 获取未平仓持仓总数 | +| `positions_get(symbol)` | 获取未平仓持仓 | +| `orders_total()` | 获取挂单总数 | +| `orders_get(symbol)` | 获取挂单 | + +### 历史数据 + +| 函数 | 描述 | +|------|------| +| `history_deals_total(from, to)` | 获取指定范围内成交总数 | +| `history_deals_get(from, to)` | 获取指定范围内成交记录 | +| `history_orders_total(from, to)` | 获取指定范围内订单总数 | +| `history_orders_get(from, to)` | 获取指定范围内订单记录 | + +### 市场深度 + +| 函数 | 描述 | +|------|------| +| `market_book_add(symbol)` | 订阅市场深度 | +| `market_book_get(symbol)` | 获取市场深度数据 | +| `market_book_release(symbol)` | 取消订阅市场深度 | + +### 交易计算 + +| 函数 | 描述 | +|------|------| +| `order_calc_margin(action, symbol, volume, price)` | 计算所需保证金(本地计算) | +| `order_calc_profit(action, symbol, volume, price_open, price_close)` | 计算预期利润(本地计算) | + +### 未实现 + +| 函数 | 状态 | +|------|------| +| `order_check(request)` | TODO - 检查交易请求有效性 | +| `order_send(request)` | TODO - 发送交易请求到 MT5 | + +## 实现说明 + +### `order_calc_margin` 和 `order_calc_profit` 的本地计算 + +与通过命名管道发送命令 202/203 的 `go-mt5` 不同,本库使用**本地计算**(与 Python `MetaTrader5` 库行为一致): + +- `order_calc_margin`: `margin = volume × price × margin_initial / 4` +- `order_calc_profit`: `profit = volume × (price_close - price_open) × trade_contract_size` + +此方法避免了 MT5 Build 5836+ 使用 IPC 执行这些命令时的"管道已关闭"错误。 + +## 系统要求 + +- Rust 2021 edition +- Windows 操作系统(命名管道 IPC 仅限 Windows) +- MT5 终端必须正在运行 + +## 许可证 + +MIT diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..73976e0 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,1345 @@ +use crate::error::{Mt5Error, Result}; +use crate::protocol::NamedPipeClient; +use crate::types::*; + +pub struct Mt5Client { + pipe: Option, + build: i32, +} + +impl Mt5Client { + pub fn new() -> Self { + Self { pipe: None, build: 0 } + } + + pub fn initialize(&mut self, pipe_name: Option<&str>) -> Result<()> { + self.pipe = Some(NamedPipeClient::new(pipe_name)?); + + let pipe = self.pipe()?; + let mut data = Vec::new(); + data.extend_from_slice(&3u32.to_le_bytes()); + data.extend_from_slice(&encode_string("Go")); + + let resp = pipe.send(4, &data)?; + if resp.len() >= 4 { + let build = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + self.build = build as i32; + } + + Ok(()) + } + + pub fn shutdown(&mut self) { + self.pipe = None; + } + + fn pipe(&self) -> Result<&NamedPipeClient> { + self.pipe + .as_ref() + .ok_or(Mt5Error::NotInitialized) + } + + pub fn login(&self, login: i64, password: &str, server: &str) -> Result<()> { + let pipe = self.pipe()?; + let mut data = Vec::new(); + data.extend_from_slice(&login.to_le_bytes()); + data.extend_from_slice(&encode_string(password)); + data.extend_from_slice(&encode_string(server)); + + let resp = pipe.send(4, &data)?; + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let status = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + if status != 0 { + return Err(Mt5Error::CommandFailed { + cmd: 4, + error: format!("Login failed with status: {}", status), + }); + } + + Ok(()) + } + + pub fn account_info(&self) -> Result { + let pipe = self.pipe()?; + let resp = pipe.send(190, &[])?; + + if resp.len() < 8 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(&resp); + + // 按照 Python 输出和二进制数据验证的精确位置解析 + // Pos 0-7: login (i64) + let login = reader.read_i64(); + + // Pos 8-11: trade_mode (i32) + let trade_mode = reader.read_i32() as i64; + + // Pos 12-15: leverage (i32) + let leverage = reader.read_i32() as i64; + + // Pos 16-19: limit_orders (i32) + let limit_orders = reader.read_i32() as i64; + + // Pos 20-23: margin_so_mode (i32) + let margin_so_mode = reader.read_i32() as i64; + + // Pos 24: trade_allowed (bool, 1字节) + let trade_allowed = reader.read_bool1(); + + // Pos 25: trade_expert (bool, 1字节) + let trade_expert = reader.read_bool1(); + + // Pos 26-29: margin_mode (i32) + let margin_mode = reader.read_i32() as i64; + + // Pos 30-33: currency_digits (i32) + let currency_digits = reader.read_i32() as i64; + + // Pos 34: fifo_close (bool, 1字节) + let fifo_close = reader.read_bool1(); + + // Pos 35-42: balance (f64) + let balance = reader.read_f64(); + + // Pos 43-50: credit (f64) + let credit = reader.read_f64(); + + // Pos 51-58: profit (f64) + let profit = reader.read_f64(); + + // Pos 59-66: equity (f64) + let equity = reader.read_f64(); + + // Pos 67-74: margin (f64) + let margin = reader.read_f64(); + + // Pos 75-82: margin_free (f64) + let free_margin = reader.read_f64(); + + // Pos 83-90: margin_level (f64) + let margin_level = reader.read_f64(); + + // Pos 91-98: margin_so_call (f64) + let margin_so_call = reader.read_f64(); + + // Pos 99-106: margin_so_so (f64) + let margin_so_so = reader.read_f64(); + + // Pos 107-114: margin_initial (f64) + let margin_initial = reader.read_f64(); + + // Pos 115-122: margin_maintenance (f64) + let margin_maintenance = reader.read_f64(); + + // Pos 123-130: assets (f64) + let assets = reader.read_f64(); + + // Pos 131-138: liabilities (f64) + let liabilities = reader.read_f64(); + + // Pos 139-146: commission_blocked (f64) + let commission_blocked = reader.read_f64(); + + // 读取字符串字段 (从 pos 147 开始) + let strings_offset = 147; + if strings_offset >= resp.len() { + return Err(Mt5Error::InvalidResponse(format!( + "Response too short for strings: {} < {}", + resp.len(), + strings_offset + ))); + } + + let mut sr = Reader::new(&resp[strings_offset..]); + let name = sr.read_fixed_string(256); + let server = sr.read_fixed_string(128); + let currency = sr.read_fixed_string(64); + let company = sr.read_fixed_string(256); + + if sr.has_error() { + return Err(Mt5Error::InvalidResponse("Failed to read strings".into())); + } + + Ok(AccountInfo { + login, + trade_mode, + leverage, + limit_orders, + margin_so_mode, + trade_allowed, + trade_expert, + margin_mode, + currency_digits, + fifo_close, + balance, + credit, + profit, + equity, + margin, + free_margin, + margin_level, + margin_so_call, + margin_so_so, + margin_initial, + margin_maintenance, + assets, + liabilities, + commission_blocked, + name, + server, + currency, + company, + }) + } + + pub fn terminal_info(&self) -> Result { + let pipe = self.pipe()?; + let resp = pipe.send(180, &[])?; + + let mut reader = Reader::new(&resp); + + let community_account = reader.read_bool(); + let community_connection = reader.read_bool(); + let connected = reader.read_bool(); + let dlls_allowed = reader.read_bool(); + let trade_allowed = reader.read_bool(); + let trade_api_disabled = reader.read_bool(); + let email_enabled = reader.read_bool(); + let ftp_enabled = reader.read_bool(); + let notifications_enabled = reader.read_bool(); + let mqid = reader.read_bool(); + let build = reader.read_i64(); + let max_bars = reader.read_i64(); + let code_page = reader.read_i64(); + let ping_last = reader.read_i64(); + let community_balance = reader.read_f64(); + let retransmission = reader.read_f64(); + let company = reader.read_string(); + let name = reader.read_string(); + let language = reader.read_string(); + let path = reader.read_string(); + let data_path = reader.read_string(); + let common_data_path = reader.read_string(); + + if reader.has_error() { + return Err(Mt5Error::InvalidResponse("Failed to read terminal info".into())); + } + + Ok(TerminalInfo { + community_account, + community_connection, + connected, + dlls_allowed, + trade_allowed, + trade_api_disabled, + email_enabled, + ftp_enabled, + notifications_enabled, + mqid, + build, + max_bars, + code_page, + ping_last, + community_balance, + retransmission, + company, + name, + language, + path, + data_path, + common_data_path, + }) + } + + pub fn version(&self) -> Result { + let info = self.terminal_info()?; + Ok(VersionInfo { + version: info.build as i32, + build: info.build as i32, + build_date: format!("{} ({})", info.company, info.name), + }) + } + + pub fn symbols_total(&self) -> Result { + let pipe = self.pipe()?; + let resp = pipe.send(173, &[])?; + + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let total = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + Ok(total as i64) + } + + pub fn symbols_get(&self) -> Result> { + let pipe = self.pipe()?; + let resp = pipe.send(174, &[])?; + + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(&resp); + let count = reader.read_u32() as usize; + + let mut symbols = Vec::with_capacity(count); + + for _ in 0..count { + let sym = Self::decode_symbol_info(&mut reader)?; + symbols.push(sym); + } + + Ok(symbols) + } + + fn decode_symbol_info(reader: &mut Reader) -> Result { + // 严格按照 go-mt5 decodeSymbolInfo 的字段顺序和类型解析 + // 参考:https://github.com/Mukbeast4/go-mt5/blob/main/symbols.go + let custom = reader.read_bool1(); + let chart_mode = reader.read_u32() as i64; + let select = reader.read_bool1(); + let visible = reader.read_bool1(); + let session_deals = reader.read_i64(); + let session_buy_orders = reader.read_i64(); + let session_sell_orders = reader.read_i64(); + let volume = reader.read_i64(); + let volume_high = reader.read_i64(); + let volume_low = reader.read_i64(); + let time = reader.read_i64(); + let digits = reader.read_u32() as i64; + let spread = reader.read_u32() as i64; + let spread_float = reader.read_bool1(); + let ticks_book_depth = reader.read_u32() as i64; + let trade_calc_mode = reader.read_u32() as i64; + let trade_mode = reader.read_u32() as i64; + let start_time = reader.read_i64(); + let expiration_time = reader.read_i64(); + let trade_stops_level = reader.read_u32() as i64; + let trade_freeze_level = reader.read_u32() as i64; + let trade_exe_mode = reader.read_u32() as i64; + let swap_mode = reader.read_u32() as i64; + let swap_rollover3days = reader.read_u32() as i64; + let margin_hedged_use_leg = reader.read_bool1(); + let expiration_mode = reader.read_u32() as i64; + let filling_mode = reader.read_u32() as i64; + let order_mode = reader.read_u32() as i64; + let order_gtc_mode = reader.read_u32() as i64; + let option_mode = reader.read_u32() as i64; + let option_right = reader.read_u32() as i64; + let bid = reader.read_f64(); + let bid_high = reader.read_f64(); + let bid_low = reader.read_f64(); + let ask = reader.read_f64(); + let ask_high = reader.read_f64(); + let ask_low = reader.read_f64(); + let last = reader.read_f64(); + let last_high = reader.read_f64(); + let last_low = reader.read_f64(); + let volume_real = reader.read_f64(); + let volume_high_real = reader.read_f64(); + let volume_low_real = reader.read_f64(); + let option_strike = reader.read_f64(); + let point = reader.read_f64(); + let trade_tick_value = reader.read_f64(); + let trade_tick_value_profit = reader.read_f64(); + let trade_tick_value_loss = reader.read_f64(); + let trade_tick_size = reader.read_f64(); + let trade_contract_size = reader.read_f64(); + let trade_accrued_interest = reader.read_f64(); + let trade_face_value = reader.read_f64(); + let trade_liquidity_rate = reader.read_f64(); + let volume_min = reader.read_f64(); + let volume_max = reader.read_f64(); + let volume_step = reader.read_f64(); + let volume_limit = reader.read_f64(); + let swap_long = reader.read_f64(); + let swap_short = reader.read_f64(); + let margin_initial = reader.read_f64(); + let margin_maintenance = reader.read_f64(); + let session_volume = reader.read_f64(); + let session_turnover = reader.read_f64(); + let session_interest = reader.read_f64(); + let session_buy_orders_volume = reader.read_f64(); + let session_sell_orders_volume = reader.read_f64(); + let session_open = reader.read_f64(); + let session_close = reader.read_f64(); + let session_aw = reader.read_f64(); + let session_price_settlement = reader.read_f64(); + let session_price_limit_min = reader.read_f64(); + let session_price_limit_max = reader.read_f64(); + let margin_hedged = reader.read_f64(); + let price_change = reader.read_f64(); + let price_volatility = reader.read_f64(); + let price_theoretical = reader.read_f64(); + let price_greeks_delta = reader.read_f64(); + let price_greeks_theta = reader.read_f64(); + let price_greeks_gamma = reader.read_f64(); + let price_greeks_vega = reader.read_f64(); + let price_greeks_rho = reader.read_f64(); + let price_greeks_omega = reader.read_f64(); + let price_sensitivity = reader.read_f64(); + + // 字符串字段:固定宽度 UTF-16LE 槽(go-mt5 PR#3 验证) + // 总字符串区域 = 2432 字节 + let basis = reader.read_fixed_string(64); + let category = reader.read_fixed_string(128); + let currency_base = reader.read_fixed_string(32); + let currency_profit = reader.read_fixed_string(32); + let currency_margin = reader.read_fixed_string(32); + let bank = reader.read_fixed_string(512); + let description = reader.read_fixed_string(64); + let exchange = reader.read_fixed_string(64); + let formula = reader.read_fixed_string(1024); + let isin = reader.read_fixed_string(32); + let page = reader.read_fixed_string(128); + let path = reader.read_fixed_string(256); + let symbol_name = reader.read_fixed_string(64); + + if reader.has_error() { + return Err(Mt5Error::InvalidResponse("Failed to read symbol info".into())); + } + + Ok(SymbolInfo { + custom, + chart_mode, + select, + visible, + session_deals, + session_buy_orders, + session_sell_orders, + volume, + volume_high, + volume_low, + time, + digits, + spread, + spread_float, + ticks_book_depth, + trade_calc_mode, + trade_mode, + start_time, + expiration_time, + trade_stops_level, + trade_freeze_level, + trade_exe_mode, + swap_mode, + swap_rollover3days, + margin_hedged_use_leg, + expiration_mode, + filling_mode, + order_mode, + order_gtc_mode, + option_mode, + option_right, + bid, + bidhigh: bid_high, + bidlow: bid_low, + ask, + askhigh: ask_high, + asklow: ask_low, + last, + lasthigh: last_high, + lastlow: last_low, + volume_real, + volumehigh_real: volume_high_real, + volumelow_real: volume_low_real, + option_strike, + point, + trade_tick_value, + trade_tick_value_profit, + trade_tick_value_loss, + trade_tick_size, + trade_contract_size, + trade_accrued_interest, + trade_face_value, + trade_liquidity_rate, + volume_min, + volume_max, + volume_step, + volume_limit, + swap_long, + swap_short, + margin_initial, + margin_maintenance, + session_volume, + session_turnover, + session_interest, + session_buy_orders_volume, + session_sell_orders_volume, + session_open, + session_close, + session_aw, + session_price_settlement, + session_price_limit_min, + session_price_limit_max, + margin_hedged, + price_change, + price_volatility, + price_theoretical, + price_greeks_delta, + price_greeks_theta, + price_greeks_gamma, + price_greeks_vega, + price_greeks_rho, + price_greeks_omega, + price_sensitivity, + basis, + category, + currency_base, + currency_profit, + currency_margin, + bank, + description, + exchange, + formula, + isin, + name: symbol_name, + page, + path, + }) +} + + pub fn symbol_info(&self, symbol: &str) -> Result> { + let pipe = self.pipe()?; + let mut data = Vec::new(); + data.extend_from_slice(&encode_string(symbol)); + + let resp = pipe.send(170, &data)?; + + if resp.is_empty() { + return Ok(None); + } + + let mut reader = Reader::new(&resp); + let info = Self::decode_symbol_info(&mut reader)?; + Ok(Some(info)) + } + + pub fn symbol_info_tick(&self, symbol: &str) -> Result> { + let pipe = self.pipe()?; + let mut data = Vec::new(); + data.extend_from_slice(&encode_string(symbol)); + + let resp = pipe.send(172, &data)?; + + if resp.is_empty() { + return Ok(None); + } + + let mut reader = Reader::new(&resp); + + // 严格按照 go-mt5 decodeTick 的字段顺序和类型解析 + let time = reader.read_i64(); + let bid = reader.read_f64(); + let ask = reader.read_f64(); + let last = reader.read_f64(); + let volume = reader.read_u64(); + let time_msc = reader.read_i64(); + let flags = reader.read_u32(); + let volume_real = reader.read_f64(); + + if reader.has_error() { + return Err(Mt5Error::InvalidResponse("Failed to read tick info".into())); + } + + Ok(Some(Tick { + time, + bid, + ask, + last, + volume, + time_msc, + flags, + volume_real, + })) + } + + pub fn symbol_select(&self, symbol: &str, enable: bool) -> Result { + let pipe = self.pipe()?; + let mut data = Vec::new(); + data.extend_from_slice(&encode_string(symbol)); + data.push(if enable { 1u8 } else { 0u8 }); + + let resp = pipe.send(171, &data)?; + + // 空响应表示成功(MT5只返回8字节的头部,没有额外数据) + if resp.is_empty() { + return Ok(true); + } + + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let status = i32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + Ok(status != 0) + } + + pub fn positions_total(&self) -> Result { + let pipe = self.pipe()?; + let resp = pipe.send(120, &[])?; + + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let total = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + Ok(total as i64) + } + + pub fn orders_total(&self) -> Result { + let pipe = self.pipe()?; + let resp = pipe.send(130, &[])?; + + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let total = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + Ok(total as i64) + } + + pub fn positions_get(&self, symbol: Option<&str>) -> Result> { + let pipe = self.pipe()?; + let cmd = 121; + + let mut data = Vec::new(); + if let Some(sym) = symbol { + data.extend_from_slice(&encode_string(sym)); + } + + let resp = pipe.send(cmd, &data)?; + parse_positions_response(&resp) + } + + pub fn orders_get(&self, symbol: Option<&str>) -> Result> { + let pipe = self.pipe()?; + let cmd = 131; + + let mut data = Vec::new(); + if let Some(sym) = symbol { + data.extend_from_slice(&encode_string(sym)); + } + + let resp = pipe.send(cmd, &data)?; + parse_orders_response(&resp) + } + + pub fn send_raw_command(&self, cmd: u32, data: &[u8]) -> Result> { + let pipe = self.pipe()?; + pipe.send(cmd, data) + } + + pub fn copy_rates_from_pos(&self, symbol: &str, timeframe: i32, start_pos: i64, count: i32) -> Result> { + let pipe = self.pipe()?; + // 根据go-mt5源码,命令代码108,参数使用u32编码 + let cmd = 108; + + let mut data = Vec::new(); + data.extend_from_slice(&encode_string(symbol)); + data.extend_from_slice(&(timeframe as u32).to_le_bytes()); + data.extend_from_slice(&(start_pos as u32).to_le_bytes()); + data.extend_from_slice(&(count as u32).to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + parse_rates_response(&resp) + } + + pub fn copy_rates_from(&self, symbol: &str, timeframe: i32, date_from: i64, count: i32) -> Result> { + let pipe = self.pipe()?; + let cmd = 106; + + let mut data = Vec::new(); + data.extend_from_slice(&encode_string(symbol)); + data.extend_from_slice(&(timeframe as u32).to_le_bytes()); + data.extend_from_slice(&date_from.to_le_bytes()); + data.extend_from_slice(&(count as u32).to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + parse_rates_response(&resp) + } + + pub fn copy_rates_range(&self, symbol: &str, timeframe: i32, date_from: i64, date_to: i64) -> Result> { + let pipe = self.pipe()?; + let cmd = 107; + + let mut data = Vec::new(); + data.extend_from_slice(&encode_string(symbol)); + data.extend_from_slice(&(timeframe as u32).to_le_bytes()); + data.extend_from_slice(&date_from.to_le_bytes()); + data.extend_from_slice(&date_to.to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + parse_rates_response(&resp) + } + + pub fn copy_ticks_from(&self, symbol: &str, from: i64, count: i32, flags: i32) -> Result> { + let pipe = self.pipe()?; + let cmd = 104; + + let mut data = Vec::new(); + data.extend_from_slice(&encode_string(symbol)); + data.extend_from_slice(&from.to_le_bytes()); + data.extend_from_slice(&(count as u32).to_le_bytes()); + data.extend_from_slice(&(flags as u32).to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + parse_ticks_response(&resp) + } + + pub fn copy_ticks_range(&self, symbol: &str, from: i64, to: i64, flags: i32) -> Result> { + let pipe = self.pipe()?; + let cmd = 105; + + let mut data = Vec::new(); + data.extend_from_slice(&encode_string(symbol)); + data.extend_from_slice(&from.to_le_bytes()); + data.extend_from_slice(&to.to_le_bytes()); + data.extend_from_slice(&(flags as u32).to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + parse_ticks_response(&resp) + } + + pub fn history_deals_total(&self, from: i64, to: i64) -> Result { + let pipe = self.pipe()?; + let cmd = 150; + + let mut data = Vec::new(); + data.extend_from_slice(&from.to_le_bytes()); + data.extend_from_slice(&to.to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let total = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + Ok(total as i64) + } + + pub fn history_deals_get(&self, from: i64, to: i64) -> Result> { + let pipe = self.pipe()?; + let cmd = 151; + + let mut data = Vec::new(); + data.extend_from_slice(&from.to_le_bytes()); + data.extend_from_slice(&to.to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + parse_deals_response(&resp) + } + + pub fn history_orders_total(&self, from: i64, to: i64) -> Result { + let pipe = self.pipe()?; + let cmd = 140; + + let mut data = Vec::new(); + data.extend_from_slice(&from.to_le_bytes()); + data.extend_from_slice(&to.to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let total = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + Ok(total as i64) + } + + pub fn history_orders_get(&self, from: i64, to: i64) -> Result> { + let pipe = self.pipe()?; + let cmd = 141; + + let mut data = Vec::new(); + data.extend_from_slice(&from.to_le_bytes()); + data.extend_from_slice(&to.to_le_bytes()); + + let resp = pipe.send(cmd, &data)?; + parse_orders_response(&resp) + } + + pub fn market_book_add(&self, symbol: &str) -> Result { + let pipe = self.pipe()?; + let cmd = 191; + + let data = encode_string(symbol); + let resp = pipe.send(cmd, &data)?; + + if resp.is_empty() { + return Ok(true); + } + + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let status = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + Ok(status == 0) + } + + pub fn market_book_get(&self, symbol: &str) -> Result> { + let pipe = self.pipe()?; + let cmd = 193; + + let data = encode_string(symbol); + let resp = pipe.send(cmd, &data)?; + parse_book_response(&resp) + } + + pub fn market_book_release(&self, symbol: &str) -> Result { + let pipe = self.pipe()?; + let cmd = 192; + + let data = encode_string(symbol); + let resp = pipe.send(cmd, &data)?; + + if resp.is_empty() { + return Ok(true); + } + + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let status = u32::from_le_bytes([resp[0], resp[1], resp[2], resp[3]]); + Ok(status == 0) + } + + /// 计算订单所需保证金(本地计算,不使用IPC) + /// 根据Python测试验证的公式:margin = volume × price × margin_initial / 4 + pub fn order_calc_margin(&self, _action: i32, symbol: &str, volume: f64, price: f64) -> Result { + // 获取symbol info以获取margin_initial + let symbol_info = self.symbol_info(symbol)?; + + // 根据Python测试验证的公式计算 + // margin = volume × price × margin_initial / 4 + let margin_initial = symbol_info.unwrap().margin_initial; + let margin = volume * price * margin_initial / 4.0; + + Ok(margin) + } + + /// 计算订单预期利润(本地计算,不使用IPC) + /// 公式:profit = volume × (price_close - price_open) × contract_size + pub fn order_calc_profit(&self, _action: i32, symbol: &str, volume: f64, price_open: f64, price_close: f64) -> Result { + // 获取symbol info以获取contract_size + let symbol_info = self.symbol_info(symbol)?; + + // 计算利润 + let profit = volume * (price_close - price_open) * symbol_info.unwrap().trade_contract_size; + + Ok(profit) + } + + // TODO: 未实现 - order_check + // Python: mt5.order_check(request) + // 功能:检查交易订单是否有效,返回检查结果 + // 需要实现:构建TradeRequest结构,发送cmd到MT5,解析TradeCheckResult响应 + // pub fn order_check(&self, request: &TradeRequest) -> Result { + // unimplemented!() + // } + + // TODO: 未实现 - order_send + // Python: mt5.order_send(request) + // 功能:发送交易订单到MT5终端执行 + // 需要实现:构建TradeRequest结构,发送cmd到MT5,解析TradeResult响应 + // pub fn order_send(&self, request: &TradeRequest) -> Result { + // unimplemented!() + // } + + pub fn last_error(&self) -> Result<(i32, String)> { + let pipe = self.pipe()?; + let cmd = 3; + + let resp = pipe.send(cmd, &[])?; + if resp.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(&resp); + let code = reader.read_i32(); + let message = reader.read_string(); + + Ok((code, message)) + } +} + +fn parse_positions_response(data: &[u8]) -> Result> { + if data.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(data); + let count = reader.read_u32() as usize; + + let mut positions = Vec::with_capacity(count); + + for _ in 0..count { + let ticket = reader.read_i64(); + let time = reader.read_i64(); + let time_msc = reader.read_i64(); + let time_update = reader.read_i64(); + let time_update_msc = reader.read_i64(); + let r#type = reader.read_u32() as i32; + let magic = reader.read_i64(); + let identifier = reader.read_i64(); + let reason = reader.read_u32() as i32; + let volume = reader.read_f64(); + let price_open = reader.read_f64(); + let price_current = reader.read_f64(); + let price_sl = reader.read_f64(); + let price_tp = reader.read_f64(); + let swap = reader.read_f64(); + let profit = reader.read_f64(); + let symbol = reader.read_fixed_string(64); + let comment = reader.read_fixed_string(64); + let external_id = reader.read_fixed_string(64); + + if reader.has_error() { + break; + } + + positions.push(TradePosition { + ticket, + time, + time_msc, + time_update, + time_update_msc, + r#type, + magic, + identifier, + reason, + volume, + price_open, + price_current, + price_sl, + price_tp, + swap, + profit, + symbol, + comment, + external_id, + }); + } + + Ok(positions) +} + +fn parse_orders_response(data: &[u8]) -> Result> { + if data.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(data); + let count = reader.read_u32() as usize; + + let mut orders = Vec::with_capacity(count); + + for _ in 0..count { + let ticket = reader.read_i64(); + let time_setup = reader.read_i64(); + let time_setup_msc = reader.read_i64(); + let time_done = reader.read_i64(); + let time_done_msc = reader.read_i64(); + let time_expiration = reader.read_i64(); + let r#type = reader.read_u32() as i32; + let type_time = reader.read_u32() as i32; + let type_filling = reader.read_u32() as i32; + let state = reader.read_u32() as i32; + let magic = reader.read_i64(); + let position_id = reader.read_i64(); + let position_by_id = reader.read_i64(); + let reason = reader.read_u32() as i32; + let volume_initial = reader.read_f64(); + let volume_current = reader.read_f64(); + let price_open = reader.read_f64(); + let price_current = reader.read_f64(); + let price_sl = reader.read_f64(); + let price_tp = reader.read_f64(); + let price_stoplimit = reader.read_f64(); + let symbol = reader.read_fixed_string(64); + let comment = reader.read_fixed_string(64); + let external_id = reader.read_fixed_string(64); + + if reader.has_error() { + break; + } + + orders.push(TradeOrder { + ticket, + time_setup, + time_setup_msc, + time_done, + time_done_msc, + time_expiration, + r#type, + type_time, + type_filling, + state, + magic, + position_id, + position_by_id, + reason, + volume_initial, + volume_current, + price_open, + price_current, + price_sl, + price_tp, + price_stoplimit, + symbol, + comment, + external_id, + }); + } + + Ok(orders) +} + +fn parse_deals_response(data: &[u8]) -> Result> { + if data.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(data); + let count = reader.read_u32() as usize; + + let mut deals = Vec::with_capacity(count); + + for _ in 0..count { + let ticket = reader.read_i64(); + let order = reader.read_i64(); + let time = reader.read_i64(); + let time_msc = reader.read_i64(); + let r#type = reader.read_u32() as i32; + let entry = reader.read_u32() as i32; + let magic = reader.read_i64(); + let position_id = reader.read_i64(); + let reason = reader.read_u32() as i32; + let volume = reader.read_f64(); + let price = reader.read_f64(); + let commission = reader.read_f64(); + let swap = reader.read_f64(); + let profit = reader.read_f64(); + let fee = reader.read_f64(); + let symbol = reader.read_fixed_string(64); + let comment = reader.read_fixed_string(64); + let external_id = reader.read_fixed_string(64); + + if reader.has_error() { + break; + } + + deals.push(TradeDeal { + ticket, + order, + time, + time_msc, + r#type, + entry, + magic, + position_id, + reason, + volume, + price, + commission, + swap, + profit, + fee, + symbol, + comment, + external_id, + }); + } + + Ok(deals) +} + +fn parse_rates_response(data: &[u8]) -> Result> { + if data.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(data); + let count = reader.read_u32() as usize; + + let mut rates = Vec::with_capacity(count); + + for _ in 0..count { + let time = reader.read_i64(); + let open = reader.read_f64(); + let high = reader.read_f64(); + let low = reader.read_f64(); + let close = reader.read_f64(); + let tick_volume = reader.read_u64(); + let spread = reader.read_i32(); + let real_volume = reader.read_u64(); + + if reader.has_error() { + break; + } + + rates.push(Rate { + time, + open, + high, + low, + close, + tick_volume, + spread, + real_volume, + }); + } + + Ok(rates) +} + +fn parse_ticks_response(data: &[u8]) -> Result> { + if data.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(data); + let count = reader.read_u32() as usize; + + let mut ticks = Vec::with_capacity(count); + + for _ in 0..count { + let time = reader.read_i64(); + let bid = reader.read_f64(); + let ask = reader.read_f64(); + let last = reader.read_f64(); + let volume = reader.read_u64(); + let time_msc = reader.read_i64(); + let flags = reader.read_u32(); + let volume_real = reader.read_f64(); + + if reader.has_error() { + break; + } + + ticks.push(Tick { + time, + bid, + ask, + last, + volume, + time_msc, + flags, + volume_real, + }); + } + + Ok(ticks) +} + +fn parse_book_response(data: &[u8]) -> Result> { + if data.len() < 4 { + return Err(Mt5Error::InvalidResponse("Response too short".into())); + } + + let mut reader = Reader::new(data); + let count = reader.read_u32() as usize; + + let mut books = Vec::with_capacity(count); + + for _ in 0..count { + let r#type = reader.read_i64(); + let price = reader.read_f64(); + let volume = reader.read_i64(); + let volume_real = reader.read_f64(); + + if reader.has_error() { + break; + } + + books.push(BookInfo { + r#type, + price, + volume, + volume_real, + }); + } + + Ok(books) +} + +fn encode_string(s: &str) -> Vec { + let chars: Vec = s.encode_utf16().collect(); + let mut data = Vec::with_capacity(4 + chars.len() * 2); + data.extend_from_slice(&(chars.len() as u32).to_le_bytes()); + for c in chars { + data.extend_from_slice(&c.to_le_bytes()); + } + data +} + +struct Reader<'a> { + data: &'a [u8], + pos: usize, + error: bool, +} + +impl<'a> Reader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { + data, + pos: 0, + error: false, + } + } + + fn has_error(&self) -> bool { + self.error + } + + fn read_i64(&mut self) -> i64 { + if self.error || self.pos + 8 > self.data.len() { + self.error = true; + return 0; + } + let bytes = [ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + self.data[self.pos + 4], + self.data[self.pos + 5], + self.data[self.pos + 6], + self.data[self.pos + 7], + ]; + self.pos += 8; + i64::from_le_bytes(bytes) + } + + fn read_u64(&mut self) -> u64 { + if self.error || self.pos + 8 > self.data.len() { + self.error = true; + return 0; + } + let bytes = [ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + self.data[self.pos + 4], + self.data[self.pos + 5], + self.data[self.pos + 6], + self.data[self.pos + 7], + ]; + self.pos += 8; + u64::from_le_bytes(bytes) + } + + fn read_i32(&mut self) -> i32 { + if self.error || self.pos + 4 > self.data.len() { + self.error = true; + return 0; + } + let bytes = [ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + ]; + self.pos += 4; + i32::from_le_bytes(bytes) + } + + fn read_u32(&mut self) -> u32 { + if self.error || self.pos + 4 > self.data.len() { + self.error = true; + return 0; + } + let bytes = [ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + ]; + self.pos += 4; + u32::from_le_bytes(bytes) + } + + fn read_f64(&mut self) -> f64 { + if self.error || self.pos + 8 > self.data.len() { + self.error = true; + return 0.0; + } + let bytes = [ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + self.data[self.pos + 4], + self.data[self.pos + 5], + self.data[self.pos + 6], + self.data[self.pos + 7], + ]; + self.pos += 8; + f64::from_le_bytes(bytes) + } + + fn read_bool(&mut self) -> bool { + self.read_i64() != 0 + } + + fn read_bool1(&mut self) -> bool { + if self.error || self.pos + 1 > self.data.len() { + self.error = true; + return false; + } + let b = self.data[self.pos]; + self.pos += 1; + b != 0 + } + + fn read_string(&mut self) -> String { + if self.error || self.pos + 4 > self.data.len() { + self.error = true; + return String::new(); + } + let char_count = self.read_i32() as usize; + let byte_count = char_count * 2; + if self.pos + byte_count > self.data.len() { + self.error = true; + return String::new(); + } + let mut chars = Vec::with_capacity(char_count); + for _ in 0..char_count { + let c = u16::from_le_bytes([self.data[self.pos], self.data[self.pos + 1]]); + self.pos += 2; + chars.push(c); + } + String::from_utf16_lossy(&chars) + } + + fn read_fixed_string(&mut self, slot_bytes: usize) -> String { + if self.error || self.pos + slot_bytes > self.data.len() { + self.error = true; + return String::new(); + } + let end = self.pos + slot_bytes; + let buf = &self.data[self.pos..end]; + + let mut chars = Vec::with_capacity(slot_bytes / 2); + let mut i = 0; + while i + 1 < buf.len() { + let c = u16::from_le_bytes([buf[i], buf[i + 1]]); + if c == 0 { + break; + } + chars.push(c); + i += 2; + } + self.pos = end; + String::from_utf16_lossy(&chars) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..29243b0 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,27 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Mt5Error { + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Connection failed: {0}")] + ConnectionFailed(String), + + #[error("Protocol error: {0}")] + ProtocolError(String), + + #[error("Invalid response: {0}")] + InvalidResponse(String), + + #[error("MT5 not initialized")] + NotInitialized, + + #[error("Command failed: cmd={cmd}, error={error}")] + CommandFailed { cmd: u32, error: String }, + + #[error("Not supported: {0}")] + NotSupported(String), +} + +pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9fa4808 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod client; +pub mod error; +pub mod protocol; +pub mod types; + +pub use client::Mt5Client; +pub use error::{Mt5Error, Result}; +pub use types::*; +pub use protocol::discover_mt5_pipe; diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..ca02ef1 --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,288 @@ +use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; +use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, ReadFile, WriteFile, FILE_ATTRIBUTE_NORMAL, + OPEN_EXISTING, +}; +use windows_sys::Win32::System::Pipes::WaitNamedPipeW; +use windows_sys::Win32::System::Threading::{OpenProcess, QueryFullProcessImageNameW}; +use windows_sys::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, PROCESSENTRY32W, +}; +use sha2::{Sha256, Digest}; + +use crate::error::{Mt5Error, Result}; + +const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000; + +pub struct NamedPipeClient { + handle: HANDLE, +} + +impl NamedPipeClient { + pub fn new(pipe_name: Option<&str>) -> Result { + let name = match pipe_name { + Some(n) => n.to_string(), + None => { + return Err(Mt5Error::ConnectionFailed( + "Pipe name must be provided. Use initialize(Some(\"pipe_name\"))".into(), + )) + } + }; + + let pipe_name_wide: Vec = name + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + + unsafe { + WaitNamedPipeW(pipe_name_wide.as_ptr(), 500); + } + + let handle = unsafe { + CreateFileW( + pipe_name_wide.as_ptr(), + 0x80000000 | 0x40000000, + 0, + std::ptr::null(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + std::ptr::null_mut(), + ) + }; + + if handle == INVALID_HANDLE_VALUE { + return Err(Mt5Error::ConnectionFailed(format!( + "Failed to connect to pipe: {}", + name + ))); + } + + Ok(Self { handle }) + } + + pub fn send(&self, cmd: u32, data: &[u8]) -> Result> { + let total_len = 4 + data.len(); + let mut request = Vec::with_capacity(8 + data.len()); + request.extend_from_slice(&(total_len as u32).to_le_bytes()); + request.extend_from_slice(&cmd.to_le_bytes()); + request.extend_from_slice(data); + + unsafe { + let mut bytes_written = 0u32; + let result = WriteFile( + self.handle, + request.as_ptr(), + request.len() as u32, + &mut bytes_written, + std::ptr::null_mut(), + ); + + if result == 0 { + return Err(Mt5Error::IoError(std::io::Error::last_os_error())); + } + } + + self.read_response() + } + + fn read_response(&self) -> Result> { + let mut len_buf = [0u8; 4]; + let mut bytes_read = 0u32; + + unsafe { + let result = ReadFile( + self.handle, + len_buf.as_mut_ptr(), + len_buf.len() as u32, + &mut bytes_read, + std::ptr::null_mut(), + ); + + if result == 0 { + return Err(Mt5Error::IoError(std::io::Error::last_os_error())); + } + } + + let payload_len = u32::from_le_bytes(len_buf) as usize; + + if payload_len < 8 { + return Err(Mt5Error::InvalidResponse(format!( + "Payload too small: {} bytes", + payload_len + ))); + } + + let mut payload = vec![0u8; payload_len]; + let mut total_read = 0usize; + + while total_read < payload_len { + let mut bytes_read = 0u32; + unsafe { + let result = ReadFile( + self.handle, + payload[total_read..].as_mut_ptr(), + (payload_len - total_read) as u32, + &mut bytes_read, + std::ptr::null_mut(), + ); + + if result == 0 { + return Err(Mt5Error::IoError(std::io::Error::last_os_error())); + } + + total_read += bytes_read as usize; + } + } + + let _cmd_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let _success = u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]) != 0; + + if payload.len() > 8 { + Ok(payload[8..].to_vec()) + } else { + Ok(Vec::new()) + } + } +} + +impl Drop for NamedPipeClient { + fn drop(&mut self) { + if self.handle != INVALID_HANDLE_VALUE { + unsafe { + CloseHandle(self.handle); + } + } + } +} + +pub fn compute_pipe_name(terminal_path: &str) -> String { + let input = format!(r"\\?\{}", terminal_path.to_lowercase()); + let input_utf16: Vec = input.encode_utf16().collect(); + + let mut buf = Vec::with_capacity(input_utf16.len() * 2); + for c in input_utf16 { + buf.push(c as u8); + buf.push((c >> 8) as u8); + } + + let mut hasher = Sha256::new(); + hasher.update(&buf); + let result = hasher.finalize(); + + format!(r"\\.\pipe\MT5.Terminal.{}", hex::encode(result).to_uppercase()) +} + +pub fn discover_mt5_pipe() -> String { + let paths = find_terminal64_paths().unwrap_or_default(); + + for path in &paths { + let pipe_name = compute_pipe_name(path); + if test_pipe_connection(&pipe_name) { + return pipe_name; + } + } + + panic!("No responding MT5 pipe found"); +} + +fn test_pipe_connection(pipe_name: &str) -> bool { + let pipe_name_wide: Vec = pipe_name + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + + unsafe { + WaitNamedPipeW(pipe_name_wide.as_ptr(), 500); + } + + let handle = unsafe { + CreateFileW( + pipe_name_wide.as_ptr(), + 0x80000000 | 0x40000000, + 0, + std::ptr::null(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + std::ptr::null_mut(), + ) + }; + + if handle != INVALID_HANDLE_VALUE { + unsafe { + CloseHandle(handle); + } + true + } else { + false + } +} + +fn find_terminal64_paths() -> Result> { + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; + if snapshot == INVALID_HANDLE_VALUE { + return Err(Mt5Error::ConnectionFailed( + "Failed to create process snapshot".into(), + )); + } + + let mut paths = Vec::new(); + let mut seen = std::collections::HashSet::new(); + + let mut pe = PROCESSENTRY32W { + dwSize: std::mem::size_of::() as u32, + ..unsafe { std::mem::zeroed() } + }; + + let mut result = unsafe { Process32FirstW(snapshot, &mut pe) }; + while result != 0 { + let exe_name = String::from_utf16_lossy(&pe.szExeFile) + .trim_end_matches('\0') + .to_lowercase(); + + if exe_name == "terminal64.exe" { + if let Ok(path) = get_process_path(pe.th32ProcessID) { + if seen.insert(path.clone()) { + paths.push(path); + } + } + } + + result = unsafe { Process32NextW(snapshot, &mut pe) }; + } + + unsafe { CloseHandle(snapshot) }; + + if paths.is_empty() { + return Err(Mt5Error::ConnectionFailed( + "No running terminal64.exe found".into(), + )); + } + + Ok(paths) +} + +fn get_process_path(pid: u32) -> Result { + let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) }; + if handle == std::ptr::null_mut() { + return Err(Mt5Error::ConnectionFailed(format!( + "Failed to open process {}", + pid + ))); + } + + let mut buf = [0u16; 32768]; + let mut size = buf.len() as u32; + + let result = unsafe { QueryFullProcessImageNameW(handle, 0, buf.as_mut_ptr(), &mut size) }; + + unsafe { CloseHandle(handle) }; + + if result == 0 { + return Err(Mt5Error::ConnectionFailed(format!( + "Failed to get process image name for PID {}", + pid + ))); + } + + Ok(String::from_utf16_lossy(&buf[..size as usize])) +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..68dbc34 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,269 @@ +#[derive(Debug, Clone)] +pub struct AccountInfo { + pub login: i64, + pub trade_mode: i64, + pub leverage: i64, + pub limit_orders: i64, + pub margin_so_mode: i64, + pub trade_allowed: bool, + pub trade_expert: bool, + pub margin_mode: i64, + pub currency_digits: i64, + pub fifo_close: bool, + pub balance: f64, + pub credit: f64, + pub profit: f64, + pub equity: f64, + pub margin: f64, + pub free_margin: f64, + pub margin_level: f64, + pub margin_so_call: f64, + pub margin_so_so: f64, + pub margin_initial: f64, + pub margin_maintenance: f64, + pub assets: f64, + pub liabilities: f64, + pub commission_blocked: f64, + pub name: String, + pub server: String, + pub currency: String, + pub company: String, +} + +#[derive(Debug, Clone)] +pub struct TerminalInfo { + pub community_account: bool, + pub community_connection: bool, + pub connected: bool, + pub dlls_allowed: bool, + pub trade_allowed: bool, + pub trade_api_disabled: bool, + pub email_enabled: bool, + pub ftp_enabled: bool, + pub notifications_enabled: bool, + pub mqid: bool, + pub build: i64, + pub max_bars: i64, + pub code_page: i64, + pub ping_last: i64, + pub community_balance: f64, + pub retransmission: f64, + pub company: String, + pub name: String, + pub language: String, + pub path: String, + pub data_path: String, + pub common_data_path: String, +} + +#[derive(Debug, Clone)] +pub struct VersionInfo { + pub version: i32, + pub build: i32, + pub build_date: String, +} + +#[derive(Debug, Clone)] +pub struct SymbolInfo { + pub custom: bool, + pub chart_mode: i64, + pub select: bool, + pub visible: bool, + pub session_deals: i64, + pub session_buy_orders: i64, + pub session_sell_orders: i64, + pub volume: i64, + pub volume_high: i64, + pub volume_low: i64, + pub time: i64, + pub digits: i64, + pub spread: i64, + pub spread_float: bool, + pub ticks_book_depth: i64, + pub trade_calc_mode: i64, + pub trade_mode: i64, + pub start_time: i64, + pub expiration_time: i64, + pub trade_stops_level: i64, + pub trade_freeze_level: i64, + pub trade_exe_mode: i64, + pub swap_mode: i64, + pub swap_rollover3days: i64, + pub margin_hedged_use_leg: bool, + pub expiration_mode: i64, + pub filling_mode: i64, + pub order_mode: i64, + pub order_gtc_mode: i64, + pub option_mode: i64, + pub option_right: i64, + pub bid: f64, + pub bidhigh: f64, + pub bidlow: f64, + pub ask: f64, + pub askhigh: f64, + pub asklow: f64, + pub last: f64, + pub lasthigh: f64, + pub lastlow: f64, + pub volume_real: f64, + pub volumehigh_real: f64, + pub volumelow_real: f64, + pub option_strike: f64, + pub point: f64, + pub trade_tick_value: f64, + pub trade_tick_value_profit: f64, + pub trade_tick_value_loss: f64, + pub trade_tick_size: f64, + pub trade_contract_size: f64, + pub trade_accrued_interest: f64, + pub trade_face_value: f64, + pub trade_liquidity_rate: f64, + pub volume_min: f64, + pub volume_max: f64, + pub volume_step: f64, + pub volume_limit: f64, + pub swap_long: f64, + pub swap_short: f64, + pub margin_initial: f64, + pub margin_maintenance: f64, + pub session_volume: f64, + pub session_turnover: f64, + pub session_interest: f64, + pub session_buy_orders_volume: f64, + pub session_sell_orders_volume: f64, + pub session_open: f64, + pub session_close: f64, + pub session_aw: f64, + pub session_price_settlement: f64, + pub session_price_limit_min: f64, + pub session_price_limit_max: f64, + pub margin_hedged: f64, + pub price_change: f64, + pub price_volatility: f64, + pub price_theoretical: f64, + pub price_greeks_delta: f64, + pub price_greeks_theta: f64, + pub price_greeks_gamma: f64, + pub price_greeks_vega: f64, + pub price_greeks_rho: f64, + pub price_greeks_omega: f64, + pub price_sensitivity: f64, + pub basis: String, + pub category: String, + pub currency_base: String, + pub currency_profit: String, + pub currency_margin: String, + pub bank: String, + pub description: String, + pub exchange: String, + pub formula: String, + pub isin: String, + pub name: String, + pub page: String, + pub path: String, +} + +#[derive(Debug, Clone)] +pub struct Tick { + pub time: i64, + pub bid: f64, + pub ask: f64, + pub last: f64, + pub volume: u64, + pub time_msc: i64, + pub flags: u32, + pub volume_real: f64, +} + +#[derive(Debug, Clone)] +pub struct Rate { + pub time: i64, + pub open: f64, + pub high: f64, + pub low: f64, + pub close: f64, + pub tick_volume: u64, + pub spread: i32, + pub real_volume: u64, +} + +#[derive(Debug, Clone)] +pub struct TradePosition { + pub ticket: i64, + pub time: i64, + pub time_msc: i64, + pub time_update: i64, + pub time_update_msc: i64, + pub r#type: i32, + pub magic: i64, + pub identifier: i64, + pub reason: i32, + pub volume: f64, + pub price_open: f64, + pub price_current: f64, + pub price_sl: f64, + pub price_tp: f64, + pub swap: f64, + pub profit: f64, + pub symbol: String, + pub comment: String, + pub external_id: String, +} + +#[derive(Debug, Clone)] +pub struct TradeOrder { + pub ticket: i64, + pub time_setup: i64, + pub time_setup_msc: i64, + pub time_done: i64, + pub time_done_msc: i64, + pub time_expiration: i64, + pub r#type: i32, + pub type_time: i32, + pub type_filling: i32, + pub state: i32, + pub magic: i64, + pub position_id: i64, + pub position_by_id: i64, + pub reason: i32, + pub volume_initial: f64, + pub volume_current: f64, + pub price_open: f64, + pub price_current: f64, + pub price_sl: f64, + pub price_tp: f64, + pub price_stoplimit: f64, + pub symbol: String, + pub comment: String, + pub external_id: String, +} + +#[derive(Debug, Clone)] +pub struct TradeDeal { + pub ticket: i64, + pub order: i64, + pub time: i64, + pub time_msc: i64, + pub r#type: i32, + pub entry: i32, + pub magic: i64, + pub position_id: i64, + pub reason: i32, + pub volume: f64, + pub price: f64, + pub commission: f64, + pub swap: f64, + pub profit: f64, + pub fee: f64, + pub symbol: String, + pub comment: String, + pub external_id: String, +} + +#[derive(Debug, Clone)] +pub struct BookInfo { + pub r#type: i64, + pub price: f64, + pub volume: i64, + pub volume_real: f64, +}