using MtApi5; using System.Reflection; var builder = WebApplication.CreateBuilder(args); builder.WebHost.UseUrls("http://0.0.0.0:8080"); builder.Services.AddSingleton(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(); var app = builder.Build(); // API Key 认证 const string API_KEY = "UiHMqtaYLZzwBdcuS4RFmEGhgDO8N2eI"; app.Use(async (context, next) => { var key = context.Request.Headers["X-API-Key"].FirstOrDefault() ?? context.Request.Query["key"].FirstOrDefault(); if (key != API_KEY) { context.Response.StatusCode = 401; await context.Response.WriteAsync("Unauthorized"); return; } await next(); }); 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(); 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(); 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(); 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(); 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 _logger; public MtConnectionService(MtApi5Client client, ILogger logger) { _client = client; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var tcs = new TaskCompletionSource(); _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(); _client.BeginConnect(8228); await tcs.Task; if (_client.ConnectionState == Mt5ConnectionState.Connected) _logger.LogInformation("MT5 reconnected."); } } } }