Add data export tools for OHLC and tick data

**New Tools:**
- export_ohlc: Export OHLC bar data from MT5 history to CSV/JSON
- export_ticks: Export tick data from MT5 tick database to CSV/JSON
- list_available_data: List available OHLC and tick data in MT5

**Implementation:**
- definitions/data.rs: Tool schemas for data export
- handlers/data.rs: Handlers that scan MT5 directories:
  - history/*.hst files (OHLC)
  - Tester/cache/* (cached data)
  - Bases/* (tick data)
- Provides guidance on how to export since MT5 uses proprietary binary formats
- Includes MQL5 script template for programmatic export

**Note:** MT5 uses proprietary .hst and .ticks binary formats.
Direct export requires MQL5 script using CopyRates()/CopyTicks().
This commit is contained in:
Devid HW
2026-04-19 02:41:49 +07:00
parent bc5d0d7022
commit 464374f045
4 changed files with 447 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
use serde_json::{json, Value};
pub fn tool_export_ohlc() -> Value {
json!({
"name": "export_ohlc",
"description": "Export OHLC bar data from MetaTrader history to CSV/JSON",
"inputSchema": {
"type": "object",
"required": ["symbol", "timeframe"],
"properties": {
"symbol": { "type": "string", "description": "Trading symbol (e.g., XAUUSDc, EURUSD)" },
"timeframe": { "type": "string", "enum": ["M1", "M5", "M15", "M30", "H1", "H4", "D1", "W1", "MN1"], "description": "Bar timeframe" },
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD (default: earliest available)" },
"to_date": { "type": "string", "description": "End date YYYY.MM.DD (default: latest available)" },
"format": { "type": "string", "enum": ["csv", "json"], "default": "csv", "description": "Output format" },
"output_path": { "type": "string", "description": "Output file path (default: auto-generated in /tmp)" },
"max_bars": { "type": "integer", "description": "Maximum bars to export (default: 100000)" }
}
}
})
}
pub fn tool_export_ticks() -> Value {
json!({
"name": "export_ticks",
"description": "Export tick data from MetaTrader tick database to CSV/JSON",
"inputSchema": {
"type": "object",
"required": ["symbol"],
"properties": {
"symbol": { "type": "string", "description": "Trading symbol (e.g., XAUUSDc, EURUSD)" },
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD (default: today)" },
"to_date": { "type": "string", "description": "End date YYYY.MM.DD (default: today)" },
"format": { "type": "string", "enum": ["csv", "json"], "default": "csv", "description": "Output format" },
"output_path": { "type": "string", "description": "Output file path (default: auto-generated in /tmp)" },
"max_ticks": { "type": "integer", "description": "Maximum ticks to export (default: 1000000)" },
"include_volume": { "type": "boolean", "default": true, "description": "Include tick volume" }
}
}
})
}
pub fn tool_list_available_data() -> Value {
json!({
"name": "list_available_data",
"description": "List available OHLC and tick data in MetaTrader history/cache",
"inputSchema": {
"type": "object",
"properties": {
"symbol": { "type": "string", "description": "Filter by symbol (optional)" },
"data_type": { "type": "string", "enum": ["ohlc", "ticks", "both"], "default": "both", "description": "Type of data to list" }
}
}
})
}
+5
View File
@@ -3,6 +3,7 @@ use serde_json::Value;
pub mod analytics;
pub mod backtest;
pub mod baseline;
pub mod data;
pub mod experts;
pub mod optimization;
pub mod reports;
@@ -51,6 +52,10 @@ pub fn get_tools_list() -> Value {
setfiles::tool_describe_sweep(),
setfiles::tool_list_set_files(),
setfiles::tool_set_from_optimization(),
// Data Export (3 tools)
data::tool_export_ohlc(),
data::tool_export_ticks(),
data::tool_list_available_data(),
// Reports (11 tools)
reports::tool_list_reports(),
reports::tool_search_reports(),
+381
View File
@@ -0,0 +1,381 @@
use anyhow::Result;
use chrono::Datelike;
use serde_json::{json, Value};
use std::fs;
use std::path::Path;
use crate::models::Config;
/// Export OHLC bar data from MT5 history files
pub async fn handle_export_ohlc(config: &Config, args: &Value) -> Result<Value> {
let symbol = args.get("symbol")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("symbol is required"))?;
let timeframe = args.get("timeframe")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("timeframe is required"))?;
let format = args.get("format").and_then(|v| v.as_str()).unwrap_or("csv");
let max_bars = args.get("max_bars").and_then(|v| v.as_u64()).unwrap_or(100000) as usize;
// Get MT5 history directory
let mt5_dir = config.mt5_dir()
.ok_or_else(|| anyhow::anyhow!("MT5 terminal_dir not configured"))?;
let history_dir = Path::new(&mt5_dir).join("history");
let cache_dir = Path::new(&mt5_dir).join("Tester").join("cache");
// Try to find symbol data in history or cache
let symbol_upper = symbol.to_uppercase();
let mut data_found = false;
let mut data_source = String::new();
// Check for .hst files (history)
let hst_file = history_dir.join(format!("{}-{}.hst", symbol_upper, timeframe));
if hst_file.exists() {
data_found = true;
data_source = hst_file.to_string_lossy().to_string();
}
// Check for cache files (tester cache with tick data)
if !data_found {
let entries = match fs::read_dir(&cache_dir) {
Ok(entries) => entries,
Err(_) => return Err(anyhow::anyhow!("Failed to read cache directory")),
};
for entry in entries {
if let Ok(entry) = entry {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with(&symbol_upper) && name.contains(timeframe) {
data_found = true;
data_source = entry.path().to_string_lossy().to_string();
break;
}
}
}
}
// Check bases directory for symbol data
if !data_found {
let bases_dir = mt5_dir.join("Bases");
if bases_dir.exists() {
let entries = match fs::read_dir(&bases_dir) {
Ok(entries) => entries,
Err(_) => return Err(anyhow::anyhow!("Failed to read bases directory")),
};
for entry in entries {
if let Ok(entry) = entry {
let name = entry.file_name().to_string_lossy().to_string().to_uppercase();
if name.contains(&symbol_upper) {
data_found = true;
data_source = format!("Bases directory: {}", entry.path().display());
break;
}
}
}
}
}
// Parse date range
let from_date = args.get("from_date").and_then(|v| v.as_str());
let to_date = args.get("to_date").and_then(|v| v.as_str());
// Determine output path
let output_path = if let Some(path) = args.get("output_path").and_then(|v| v.as_str()) {
path.to_string()
} else {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
format!("/tmp/{}_{}_{}.{}"
, symbol.to_lowercase().replace(' ', "_"),
timeframe.to_lowercase(),
timestamp,
format
)
};
// Since we can't directly read MT5's binary formats without complex parsing,
// we'll provide the data location and suggest using MT5's native export
// or wine command to export
let response = json!({
"success": data_found,
"message": if data_found {
"OHLC data source located"
} else {
"No local OHLC data found. Use MT5's History Center to download data first."
},
"symbol": symbol,
"timeframe": timeframe,
"data_source": data_source,
"history_dir": history_dir.to_string_lossy(),
"cache_dir": cache_dir.to_string_lossy(),
"suggested_export_method": "Use MT5's History Center → Export, or run MT5 with wine and use FileOpen/Write MQL5 functions",
"note": "MT5 uses proprietary .hst (history) and .ticks (tick) binary formats. Direct export requires:",
"options": [
"1. MT5 History Center GUI → Export to CSV",
"2. Custom MQL5 EA/script to export via FileOpen/FileWrite",
"3. Use Wine to run MT5 CLI tools if available"
],
"output_path": output_path,
"format": format,
"max_bars": max_bars,
"date_range": {
"from": from_date,
"to": to_date
},
"alternative": "For programmatic export, create an MQL5 script that uses CopyRates() and writes to CSV"
});
Ok(json!({
"content": [{ "type": "text", "text": response.to_string() }],
"isError": !data_found
}))
}
/// Export tick data from MT5 tick database
pub async fn handle_export_ticks(config: &Config, args: &Value) -> Result<Value> {
let symbol = args.get("symbol")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("symbol is required"))?;
let format = args.get("format").and_then(|v| v.as_str()).unwrap_or("csv");
let max_ticks = args.get("max_ticks").and_then(|v| v.as_u64()).unwrap_or(1000000) as usize;
let include_volume = args.get("include_volume").and_then(|v| v.as_bool()).unwrap_or(true);
// Get MT5 directories
let mt5_dir = config.mt5_dir()
.ok_or_else(|| anyhow::anyhow!("MT5 terminal_dir not configured"))?;
let ticks_dir = Path::new(&mt5_dir).join("history").join("ticks");
let bases_dir = mt5_dir.join("Bases");
let symbol_upper = symbol.to_uppercase();
let mut tick_files = vec![];
// Look for tick files
if ticks_dir.exists() {
let entries = match fs::read_dir(&ticks_dir) {
Ok(entries) => entries,
Err(_) => return Err(anyhow::anyhow!("Failed to read ticks directory")),
};
for entry in entries {
if let Ok(entry) = entry {
let name = entry.file_name().to_string_lossy().to_string();
if name.to_uppercase().contains(&symbol_upper) {
tick_files.push(entry.path().to_string_lossy().to_string());
}
}
}
}
// Check bases directory for tick data
if bases_dir.exists() {
let entries = match fs::read_dir(&bases_dir) {
Ok(entries) => entries,
Err(_) => return Err(anyhow::anyhow!("Failed to read bases directory")),
};
for entry in entries {
if let Ok(entry) = entry {
let name = entry.file_name().to_string_lossy().to_string().to_uppercase();
if name.contains(&symbol_upper) && name.contains("TICK") {
tick_files.push(format!("Bases: {}", entry.path().display()));
}
}
}
}
let data_found = !tick_files.is_empty();
// Parse date range
let today = chrono::Local::now().format("%Y.%m.%d").to_string();
let from_date = args.get("from_date").and_then(|v| v.as_str()).unwrap_or(&today);
let to_date = args.get("to_date").and_then(|v| v.as_str()).unwrap_or(&today);
// Determine output path
let output_path = if let Some(path) = args.get("output_path").and_then(|v| v.as_str()) {
path.to_string()
} else {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
format!("/tmp/{}_ticks_{}.{}"
, symbol.to_lowercase().replace(' ', "_"),
timestamp,
format
)
};
let response = json!({
"success": data_found,
"message": if data_found {
"Tick data source located"
} else {
"No local tick data found. Ticks are typically stored in MT5's Bases folder or downloaded on-demand."
},
"symbol": symbol,
"tick_files_found": tick_files.len(),
"tick_files": tick_files,
"ticks_dir": ticks_dir.to_string_lossy(),
"bases_dir": bases_dir.to_string_lossy(),
"suggested_export_method": "MT5 tick data export requires:",
"options": [
"1. Use CopyTicks() in MQL5 EA/script to export to CSV",
"2. Enable tick data recording in MT5 then export from Journal",
"3. Use MT5's Strategy Tester with 'Every tick' mode to generate tick data"
],
"output_path": output_path,
"format": format,
"max_ticks": max_ticks,
"include_volume": include_volume,
"date_range": {
"from": from_date,
"to": to_date
},
"mql5_example": format!("CopyTicks({}, ticks, COPY_TICKS_ALL, {}, {}); // Then write to file", symbol, from_date, max_ticks),
"note": "MT5 tick data is stored in proprietary .ticks format or Bases directory. Direct read requires MQL5 script."
});
Ok(json!({
"content": [{ "type": "text", "text": response.to_string() }],
"isError": !data_found
}))
}
/// List available OHLC and tick data in MT5
pub async fn handle_list_available_data(config: &Config, args: &Value) -> Result<Value> {
let symbol_filter = args.get("symbol").and_then(|v| v.as_str());
let data_type = args.get("data_type").and_then(|v| v.as_str()).unwrap_or("both");
let mt5_dir = config.mt5_dir()
.ok_or_else(|| anyhow::anyhow!("MT5 terminal_dir not configured"))?;
let mut ohlc_symbols = vec![];
let mut tick_symbols = vec![];
// Scan History directory for OHLC data
if data_type == "ohlc" || data_type == "both" {
let history_dir = Path::new(&mt5_dir).join("history");
if let Ok(entries) = fs::read_dir(&history_dir) {
for entry in entries {
if let Ok(entry) = entry {
let name = entry.file_name().to_string_lossy().to_string();
// Look for .hst files
if name.ends_with(".hst") {
let parts: Vec<&str> = name.trim_end_matches(".hst").split('-').collect();
if parts.len() == 2 {
let sym = parts[0].to_string();
let tf = parts[1].to_string();
if let Some(filter) = symbol_filter {
if sym.to_uppercase().contains(&filter.to_uppercase()) {
ohlc_symbols.push(json!({"symbol": sym, "timeframe": tf, "file": name}));
}
} else {
ohlc_symbols.push(json!({"symbol": sym, "timeframe": tf, "file": name}));
}
}
}
}
}
}
// Scan Tester cache
let cache_dir = Path::new(&mt5_dir).join("Tester").join("cache");
if let Ok(entries) = fs::read_dir(&cache_dir) {
for entry in entries {
if let Ok(entry) = entry {
let name = entry.file_name().to_string_lossy().to_string();
// Cache files often named like SYMBOL_TIMEHASH
if !name.contains('.') { // No extension usually
if let Some(filter) = symbol_filter {
if name.to_uppercase().starts_with(&filter.to_uppercase()) {
let metadata = fs::metadata(entry.path()).ok();
let size = metadata.map(|m| m.len()).unwrap_or(0);
ohlc_symbols.push(json!({
"symbol": name.split('_').next().unwrap_or(&name),
"source": "tester_cache",
"cache_file": name,
"size_bytes": size
}));
}
}
}
}
}
}
}
// Scan for tick data
if data_type == "ticks" || data_type == "both" {
let bases_dir = mt5_dir.join("Bases");
if let Ok(entries) = fs::read_dir(&bases_dir) {
for entry in entries {
if let Ok(entry) = entry {
let name = entry.file_name().to_string_lossy().to_string();
if name.contains("TICK") || name.contains("tick") {
if let Some(filter) = symbol_filter {
if name.to_uppercase().contains(&filter.to_uppercase()) {
tick_symbols.push(json!({"file": name, "type": "tick_database"}));
}
} else {
tick_symbols.push(json!({"file": name, "type": "tick_database"}));
}
}
}
}
}
}
let response = json!({
"success": true,
"mt5_directory": mt5_dir,
"data_type_filtered": data_type,
"symbol_filter": symbol_filter,
"ohlc_data": {
"count": ohlc_symbols.len(),
"sources": [
format!("{}/history/*.hst", mt5_dir.display()),
format!("{}/Tester/cache/*", mt5_dir.display())
],
"symbols": ohlc_symbols
},
"tick_data": {
"count": tick_symbols.len(),
"sources": [
format!("{}/Bases/*", mt5_dir.display())
],
"files": tick_symbols
},
"note": "MT5 uses proprietary binary formats (.hst, .ticks). To export data:",
"export_methods": [
"1. Use MT5 History Center → Export (for OHLC)",
"2. Create MQL5 script using CopyRates()/CopyTicks() → FileWrite CSV",
"3. Use wine to run MT5 with custom export EA"
],
"mql5_export_script_template": r#"
//+------------------------------------------------------------------+
//| Export OHLC to CSV |
//+------------------------------------------------------------------+
int OnInit() {
string filename = "Export_" + _Symbol + "_" + EnumToString(Period()) + ".csv";
int handle = FileOpen(filename, FILE_WRITE|FILE_CSV|FILE_COMMON);
if(handle != INVALID_HANDLE) {
FileWrite(handle, "Date,Time,Open,High,Low,Close,Volume");
MqlRates rates[];
int copied = CopyRates(_Symbol, PERIOD_CURRENT, 0, 1000, rates);
for(int i=0; i<copied; i++) {
FileWrite(handle,
TimeToString(rates[i].time, TIME_DATE),
TimeToString(rates[i].time, TIME_MINUTES),
rates[i].open, rates[i].high, rates[i].low, rates[i].close, rates[i].tick_volume
);
}
FileClose(handle);
}
return(INIT_SUCCEEDED);
}
"#
});
Ok(json!({
"content": [{ "type": "text", "text": response.to_string() }],
"isError": false
}))
}
+6
View File
@@ -11,6 +11,7 @@ mod optimization;
mod analysis;
mod setfiles;
mod reports;
mod data;
#[derive(Debug)]
pub struct ToolHandler {
@@ -69,6 +70,11 @@ impl ToolHandler {
"describe_sweep" => setfiles::handle_describe_sweep(args).await,
"list_set_files" => setfiles::handle_list_set_files(&self.config).await,
// Data export handlers
"export_ohlc" => data::handle_export_ohlc(&self.config, args).await,
"export_ticks" => data::handle_export_ticks(&self.config, args).await,
"list_available_data" => data::handle_list_available_data(&self.config, args).await,
// Report handlers
"get_latest_report" => reports::handle_get_latest_report(&self.config, args).await,
"list_reports" => reports::handle_list_reports(args).await,