Files
my-Mt5Bridge/Program.cs
T
2026-07-03 15:30:55 +08:00

386 lines
16 KiB
C#

using MtApi5;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://localhost:8080");
builder.Services.AddSingleton<MtApi5Client>(sp =>
{
var client = new MtApi5Client();
try
{
var prop = typeof(MtApi5Client).GetProperty("CommandTimeout");
if (prop != null && prop.CanWrite)
{
prop.SetValue(client, 120000);
}
else
{
var field = typeof(MtApi5Client).GetField("_command_timeout",
BindingFlags.NonPublic | BindingFlags.Instance);
field?.SetValue(client, 120000);
}
Console.WriteLine("CommandTimeout set to 120000ms");
}
catch (Exception ex)
{
Console.WriteLine($"Could not set CommandTimeout: {ex.Message}");
}
return client;
});
builder.Services.AddHostedService<MtConnectionService>();
var app = builder.Build();
app.MapGet("/health", (MtApi5Client mt) =>
{
var connected = mt.ConnectionState == Mt5ConnectionState.Connected;
return new { status = connected ? "healthy" : "disconnected", mt5_connected = connected, mt5_version = "unknown", api_version = "1.0.0" };
});
app.MapGet("/account", (MtApi5Client mt) =>
{
var login = (ulong)mt.AccountInfoInteger(ENUM_ACCOUNT_INFO_INTEGER.ACCOUNT_LOGIN);
var leverage = (uint)mt.AccountInfoInteger(ENUM_ACCOUNT_INFO_INTEGER.ACCOUNT_LEVERAGE);
var tradeAllowed = mt.AccountInfoInteger(ENUM_ACCOUNT_INFO_INTEGER.ACCOUNT_TRADE_ALLOWED) != 0;
var tradeExpert = mt.AccountInfoInteger(ENUM_ACCOUNT_INFO_INTEGER.ACCOUNT_TRADE_EXPERT) != 0;
var currency = mt.AccountInfoString(ENUM_ACCOUNT_INFO_STRING.ACCOUNT_CURRENCY) ?? "USD";
var server = mt.AccountInfoString(ENUM_ACCOUNT_INFO_STRING.ACCOUNT_SERVER) ?? "";
var name = mt.AccountInfoString(ENUM_ACCOUNT_INFO_STRING.ACCOUNT_NAME) ?? "";
var company = mt.AccountInfoString(ENUM_ACCOUNT_INFO_STRING.ACCOUNT_COMPANY) ?? "";
var balance = mt.AccountInfoDouble(ENUM_ACCOUNT_INFO_DOUBLE.ACCOUNT_BALANCE);
var equity = mt.AccountInfoDouble(ENUM_ACCOUNT_INFO_DOUBLE.ACCOUNT_EQUITY);
var profit = mt.AccountInfoDouble(ENUM_ACCOUNT_INFO_DOUBLE.ACCOUNT_PROFIT);
var margin = mt.AccountInfoDouble(ENUM_ACCOUNT_INFO_DOUBLE.ACCOUNT_MARGIN);
var marginFree = mt.AccountInfoDouble(ENUM_ACCOUNT_INFO_DOUBLE.ACCOUNT_MARGIN_FREE);
var marginLevel = mt.AccountInfoDouble(ENUM_ACCOUNT_INFO_DOUBLE.ACCOUNT_MARGIN_LEVEL);
return new
{
data = new[]
{
new { login, leverage, trade_allowed = tradeAllowed, trade_expert = tradeExpert, currency, currency_digits = 2, server, name, company, balance, equity, profit, margin, margin_free = marginFree, margin_level = marginLevel }
},
count = 1,
format = "json"
};
});
app.MapGet("/symbols/{symbol}", (string symbol, MtApi5Client mt) =>
{
try
{
var digits = (int)mt.SymbolInfoInteger(symbol, ENUM_SYMBOL_INFO_INTEGER.SYMBOL_DIGITS);
var spreadFloat = mt.SymbolInfoInteger(symbol, ENUM_SYMBOL_INFO_INTEGER.SYMBOL_SPREAD_FLOAT) != 0;
var spread = (int)mt.SymbolInfoInteger(symbol, ENUM_SYMBOL_INFO_INTEGER.SYMBOL_SPREAD);
var point = mt.SymbolInfoDouble(symbol, ENUM_SYMBOL_INFO_DOUBLE.SYMBOL_POINT);
var bid = mt.SymbolInfoDouble(symbol, ENUM_SYMBOL_INFO_DOUBLE.SYMBOL_BID);
var ask = mt.SymbolInfoDouble(symbol, ENUM_SYMBOL_INFO_DOUBLE.SYMBOL_ASK);
var volumeMin = mt.SymbolInfoDouble(symbol, ENUM_SYMBOL_INFO_DOUBLE.SYMBOL_VOLUME_MIN);
var volumeMax = mt.SymbolInfoDouble(symbol, ENUM_SYMBOL_INFO_DOUBLE.SYMBOL_VOLUME_MAX);
var volumeStep = mt.SymbolInfoDouble(symbol, ENUM_SYMBOL_INFO_DOUBLE.SYMBOL_VOLUME_STEP);
var contractSize = mt.SymbolInfoDouble(symbol, ENUM_SYMBOL_INFO_DOUBLE.SYMBOL_TRADE_CONTRACT_SIZE);
string description = "", currencyBase = "", currencyProfit = "", path = "";
try { description = mt.SymbolInfoString(symbol, ENUM_SYMBOL_INFO_STRING.SYMBOL_DESCRIPTION) ?? ""; } catch { }
try { currencyBase = mt.SymbolInfoString(symbol, ENUM_SYMBOL_INFO_STRING.SYMBOL_CURRENCY_BASE) ?? ""; } catch { }
try { currencyProfit = mt.SymbolInfoString(symbol, ENUM_SYMBOL_INFO_STRING.SYMBOL_CURRENCY_PROFIT) ?? ""; } catch { }
try { path = mt.SymbolInfoString(symbol, ENUM_SYMBOL_INFO_STRING.SYMBOL_PATH) ?? ""; } catch { }
return Results.Ok(new
{
data = new[]
{
new { name = symbol, description, digits, point, bid, ask, spread, spread_float = spreadFloat, volume_min = volumeMin, volume_max = volumeMax, volume_step = volumeStep, trade_contract_size = contractSize, currency_base = currencyBase, currency_profit = currencyProfit, category = path }
},
count = 1,
format = "json"
});
}
catch (Exception ex)
{
Console.WriteLine($"ERROR /symbols/{symbol}: {ex.Message}");
return Results.Problem($"Error: {ex.Message}");
}
});
app.MapGet("/symbols/{symbol}/tick", (string symbol, MtApi5Client mt) =>
{
var tick = mt.SymbolInfoTick(symbol);
if (tick == null) return Results.NotFound(new { detail = $"Tick for {symbol} not found" });
return Results.Json(new
{
data = new[]
{
new
{
time = tick.time.ToString("yyyy-MM-ddTHH:mm:ss"),
bid = tick.bid,
ask = tick.ask,
last = tick.last,
volume = tick.volume,
time_msc = tick.time.ToString("yyyy-MM-ddTHH:mm:ss.fff000"),
flags = 6,
volume_real = tick.volume_real
}
},
count = 1,
format = "json"
});
});
app.MapGet("/rates/from-pos", (string symbol, string timeframe, int start_pos, int count, MtApi5Client mt) =>
{
try
{
var tf = timeframe switch
{
"TIMEFRAME_M1" => ENUM_TIMEFRAMES.PERIOD_M1,
"TIMEFRAME_M5" => ENUM_TIMEFRAMES.PERIOD_M5,
"TIMEFRAME_M15" => ENUM_TIMEFRAMES.PERIOD_M15,
"TIMEFRAME_M30" => ENUM_TIMEFRAMES.PERIOD_M30,
"TIMEFRAME_H1" => ENUM_TIMEFRAMES.PERIOD_H1,
"TIMEFRAME_H4" => ENUM_TIMEFRAMES.PERIOD_H4,
"TIMEFRAME_D1" => ENUM_TIMEFRAMES.PERIOD_D1,
_ => ENUM_TIMEFRAMES.PERIOD_M5
};
const int chunkSize = 1000;
var allRates = new List<MqlRates>();
int remaining = count;
int currentPos = start_pos;
while (remaining > 0)
{
int chunk = Math.Min(remaining, chunkSize);
var result = mt.CopyRates(symbol, tf, currentPos, chunk, out MqlRates[]? rates);
Console.WriteLine($"CopyRates pos={currentPos} chunk={chunk} result={result}, rates={(rates != null ? rates.Length.ToString() : "null")}");
if (rates != null && rates.Length > 0)
{
allRates.AddRange(rates);
currentPos += rates.Length;
remaining -= rates.Length;
if (rates.Length < chunk) break;
}
else
{
break;
}
}
var data = allRates.Select(r => new
{
time = r.time.ToString("yyyy-MM-ddTHH:mm:ss"),
r.open,
r.high,
r.low,
r.close,
tick_volume = r.tick_volume,
r.spread,
real_volume = r.real_volume
}).ToArray();
return Results.Ok(new { data, count = data.Length, format = "json" });
}
catch (Exception ex)
{
Console.WriteLine($"ERROR /rates/from-pos: {ex.Message}\n{ex.StackTrace}");
return Results.Problem($"Error: {ex.Message}");
}
});
app.MapGet("/positions", (MtApi5Client mt) =>
{
var total = mt.PositionsTotal();
var positions = new List<object>();
for (int i = 0; i < total; i++)
{
var ticket = mt.PositionGetTicket(i);
if (ticket == 0) continue;
positions.Add(new
{
ticket,
symbol = mt.PositionGetString(ENUM_POSITION_PROPERTY_STRING.POSITION_SYMBOL),
type = (int)mt.PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER.POSITION_TYPE),
volume = mt.PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE.POSITION_VOLUME),
price_open = mt.PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE.POSITION_PRICE_OPEN),
sl = mt.PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE.POSITION_SL),
tp = mt.PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE.POSITION_TP),
price_current = mt.PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE.POSITION_PRICE_CURRENT),
swap = mt.PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE.POSITION_SWAP),
profit = mt.PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE.POSITION_PROFIT),
comment = mt.PositionGetString(ENUM_POSITION_PROPERTY_STRING.POSITION_COMMENT),
magic = (ulong)mt.PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER.POSITION_MAGIC)
});
}
return new { data = positions, count = positions.Count, format = "json" };
});
app.MapGet("/orders", (string? symbol, MtApi5Client mt) =>
{
var total = mt.OrdersTotal();
var orders = new List<object>();
for (int i = 0; i < total; i++)
{
var ticket = mt.OrderGetTicket(i);
if (ticket == 0) continue;
var orderSymbol = mt.OrderGetString(ENUM_ORDER_PROPERTY_STRING.ORDER_SYMBOL);
if (symbol != null && orderSymbol != symbol) continue;
orders.Add(new
{
ticket,
symbol = orderSymbol,
type = (int)mt.OrderGetInteger(ENUM_ORDER_PROPERTY_INTEGER.ORDER_TYPE),
volume_initial = mt.OrderGetDouble(ENUM_ORDER_PROPERTY_DOUBLE.ORDER_VOLUME_INITIAL),
price_open = mt.OrderGetDouble(ENUM_ORDER_PROPERTY_DOUBLE.ORDER_PRICE_OPEN),
sl = mt.OrderGetDouble(ENUM_ORDER_PROPERTY_DOUBLE.ORDER_SL),
tp = mt.OrderGetDouble(ENUM_ORDER_PROPERTY_DOUBLE.ORDER_TP),
magic = (ulong)mt.OrderGetInteger(ENUM_ORDER_PROPERTY_INTEGER.ORDER_MAGIC),
comment = mt.OrderGetString(ENUM_ORDER_PROPERTY_STRING.ORDER_COMMENT)
});
}
return new { data = orders, count = orders.Count, format = "json" };
});
app.MapPost("/order/check", (TradeRequestDto body, MtApi5Client mt) =>
{
var request = ToMqlTradeRequest(body);
mt.OrderCheck(request, out MqlTradeCheckResult? checkResult);
if (checkResult == null) return new { data = new { retcode = 0u, balance = 0.0, equity = 0.0, profit = 0.0, margin = 0.0, margin_free = 0.0, margin_level = 0.0, comment = "Check failed" }, count = 1, format = "json" };
return new { data = new { retcode = checkResult.Retcode, balance = checkResult.Balance, equity = checkResult.Equity, profit = checkResult.Profit, margin = checkResult.Margin, margin_free = checkResult.Margin_free, margin_level = checkResult.Margin_level, comment = checkResult.Comment ?? "" }, count = 1, format = "json" };
});
app.MapPost("/order/send", (TradeRequestBody body, MtApi5Client mt) =>
{
var request = ToMqlTradeRequest(body.request);
mt.OrderSend(request, out MqlTradeResult? result);
if (result == null) return new { data = new { retcode = 0u, order = 0ul, comment = "Send failed" }, count = 1, format = "json" };
return new { data = new { retcode = result.Retcode, order = result.Order, comment = result.Comment ?? "" }, count = 1, format = "json" };
});
app.MapGet("/history/deals", (string date_from, string date_to, string? symbol, MtApi5Client mt) =>
{
var fromDt = DateTime.Parse(date_from);
var toDt = DateTime.Parse(date_to);
mt.HistorySelect(fromDt, toDt);
var total = mt.HistoryDealsTotal();
var dealList = new List<object>();
for (int i = 0; i < total; i++)
{
var ticket = mt.HistoryDealGetTicket(i);
if (ticket == 0) continue;
var dealSymbol = mt.HistoryDealGetString(ticket, ENUM_DEAL_PROPERTY_STRING.DEAL_SYMBOL);
if (symbol != null && dealSymbol != symbol) continue;
var dealTime = MtTimeToDateTime(mt.HistoryDealGetInteger(ticket, ENUM_DEAL_PROPERTY_INTEGER.DEAL_TIME));
dealList.Add(new
{
ticket,
time = dealTime.ToString("yyyy-MM-ddTHH:mm:ss"),
entry = (int)mt.HistoryDealGetInteger(ticket, ENUM_DEAL_PROPERTY_INTEGER.DEAL_ENTRY),
magic = (ulong)mt.HistoryDealGetInteger(ticket, ENUM_DEAL_PROPERTY_INTEGER.DEAL_MAGIC),
volume = mt.HistoryDealGetDouble(ticket, ENUM_DEAL_PROPERTY_DOUBLE.DEAL_VOLUME),
price = mt.HistoryDealGetDouble(ticket, ENUM_DEAL_PROPERTY_DOUBLE.DEAL_PRICE),
profit = mt.HistoryDealGetDouble(ticket, ENUM_DEAL_PROPERTY_DOUBLE.DEAL_PROFIT),
commission = mt.HistoryDealGetDouble(ticket, ENUM_DEAL_PROPERTY_DOUBLE.DEAL_COMMISSION),
swap = mt.HistoryDealGetDouble(ticket, ENUM_DEAL_PROPERTY_DOUBLE.DEAL_SWAP),
symbol = dealSymbol,
comment = mt.HistoryDealGetString(ticket, ENUM_DEAL_PROPERTY_STRING.DEAL_COMMENT) ?? ""
});
}
return new { data = dealList, count = dealList.Count, format = "json" };
});
app.Run();
DateTime MtTimeToDateTime(long mtTime)
{
return new DateTime(1970, 1, 1).AddSeconds(mtTime);
}
MqlTradeRequest ToMqlTradeRequest(TradeRequestDto r)
{
return new MqlTradeRequest
{
Action = (ENUM_TRADE_REQUEST_ACTIONS)r.action,
Symbol = r.symbol,
Volume = r.volume ?? 0,
Type = (ENUM_ORDER_TYPE)(r.order_type ?? 0),
Price = r.price ?? 0,
Sl = r.sl ?? 0,
Tp = r.tp ?? 0,
Magic = r.magic ?? 0,
Comment = r.comment ?? "",
Order = r.order ?? 0,
Position = r.position ?? 0,
Deviation = r.deviation ?? 0
};
}
class TradeRequestDto
{
public uint action { get; set; }
public string symbol { get; set; } = "";
public double? volume { get; set; }
public uint? order_type { get; set; }
public double? price { get; set; }
public double? sl { get; set; }
public double? tp { get; set; }
public ulong? magic { get; set; }
public string? comment { get; set; }
public ulong? order { get; set; }
public ulong? position { get; set; }
public ulong? deviation { get; set; }
}
class TradeRequestBody
{
public TradeRequestDto request { get; set; } = new();
}
class MtConnectionService : BackgroundService
{
private readonly MtApi5Client _client;
private readonly ILogger<MtConnectionService> _logger;
public MtConnectionService(MtApi5Client client, ILogger<MtConnectionService> logger)
{
_client = client;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var tcs = new TaskCompletionSource<bool>();
_client.ConnectionStateChanged += (s, e) =>
{
_logger.LogInformation("MT5 Connection: {Status}", e.Status);
if (e.Status == Mt5ConnectionState.Connected || e.Status == Mt5ConnectionState.Failed || e.Status == Mt5ConnectionState.Disconnected)
tcs.TrySetResult(e.Status == Mt5ConnectionState.Connected);
};
_logger.LogInformation("Connecting to MT5 via MtApi5 on port 8228...");
_client.BeginConnect(8228);
await tcs.Task;
if (_client.ConnectionState != Mt5ConnectionState.Connected)
{
_logger.LogError("Failed to connect to MT5. Ensure MtApi5 EA is loaded in MT5.");
return;
}
_logger.LogInformation("MT5 Bridge ready on http://localhost:8080");
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(30000, stoppingToken);
if (_client.ConnectionState != Mt5ConnectionState.Connected)
{
_logger.LogWarning("MT5 disconnected, attempting reconnect...");
tcs = new TaskCompletionSource<bool>();
_client.BeginConnect(8228);
await tcs.Task;
if (_client.ConnectionState == Mt5ConnectionState.Connected)
_logger.LogInformation("MT5 reconnected.");
}
}
}
}