diff --git a/API使用指南.md b/API使用指南.md index 56fb23c..3d7e5b1 100644 --- a/API使用指南.md +++ b/API使用指南.md @@ -309,6 +309,12 @@ for d in deals: > 让 MQL5 指标/EA 把信号写出来,Python 通过 Bridge 读取。不用翻译 MQL5 代码。 +说明: + +- 这部分属于“MT5 指标/EA 信号导出”能力,不影响 Bridge 的账户、行情、历史、持仓、挂单、预检、下单等基础 API。 +- 如果你只更新了远程 `Mt5Bridge.dll`,没有替换 `Alpha Trend.ex5`,Bridge 仍然可以正常工作,`/gvar` 也仍会返回旧版指标写出的变量名。 +- 只有在你需要“已收盘 K 线信号”和“带周期/参数作用域的新变量名”时,才需要重新编译并替换新版 `Alpha Trend.ex5`。 + #### 列出所有全局变量 ``` @@ -357,12 +363,15 @@ DELETE /gvar/{name} ## MQL5 指标 → Python 完整流程 -### 第一步:改指标源码,加一行输出 +### 第一步:改指标源码,输出已收盘 K 线信号 + +这一步是“升级指标导出行为”的可选步骤,不是 Bridge 基础 API 的必需步骤。 ```mql5 // 在 OnCalculate 末尾加 -int last = rates_total - 1; -GlobalVariableSet("MY_SIGNAL_" + _Symbol, signal_value); +int last_closed = rates_total - 2; +string key = StringFormat("MY_SIGNAL_%s_%s", _Symbol, EnumToString(_Period)); +GlobalVariableSet(key, signal_value[last_closed]); ``` ### 第二步:Python 读取信号 @@ -370,7 +379,7 @@ GlobalVariableSet("MY_SIGNAL_" + _Symbol, signal_value); ```python def read_signal(): try: - return api(f"/gvar/MY_SIGNAL_XAUUSDc")["value"] + return api(f"/gvar/MY_SIGNAL_XAUUSDc_PERIOD_H1")["value"] except: return None @@ -378,6 +387,13 @@ signal = read_signal() print(f"指标信号: {signal}") ``` +如果你仍在使用旧版 `Alpha Trend.ex5`,则读取方式应继续对应旧键名,例如: + +```python +trend = api("/gvar/AT_Trend_XAUUSDc")["value"] +buy_signal = api("/gvar/AT_Buy_XAUUSDc")["value"] +``` + ### 第三步:根据信号做决策 ```python @@ -487,9 +503,9 @@ class MAStrategy: df["ma_fast"] = df["close"].rolling(self.fast).mean() df["ma_slow"] = df["close"].rolling(self.slow).mean() - # 最新两根 K 线 - prev = df.iloc[-2] - curr = df.iloc[-1] + # 使用最近两根已收盘 K 线 + prev = df.iloc[-3] + curr = df.iloc[-2] # 金叉 if prev["ma_fast"] <= prev["ma_slow"] and curr["ma_fast"] > curr["ma_slow"]: @@ -567,4 +583,4 @@ API Key 错误或没带。检查 Header 中的 `X-API-Key` 或 URL 中的 `?key= 保证金不足,减小手数或检查 `account.margin_free`。 ### 返回 "无法连接到远程服务器" -Bridge 未运行或网络不通,先检查 `/health`。 \ No newline at end of file +Bridge 未运行或网络不通,先检查 `/health`。 diff --git a/Alpha Trend.mq5 b/Alpha Trend.mq5 index 3a41627..00422bf 100644 --- a/Alpha Trend.mq5 +++ b/Alpha Trend.mq5 @@ -67,6 +67,7 @@ double atr_multiplier; int TR_Handle; int RSI_Handle; int MFI_Handle; +string g_signal_prefix; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ @@ -127,11 +128,33 @@ int OnInit() PlotIndexSetInteger(1, PLOT_ARROW_SHIFT, 20); PlotIndexSetInteger(2, PLOT_ARROW_SHIFT, -20); //--- create handles - TR_Handle = iATR(_Symbol, _Period, 1); - RSI_Handle = iRSI(_Symbol, _Period, length, inp_price); - MFI_Handle = iMFI(_Symbol, _Period, length, VOLUME_TICK); + TR_Handle = iATR(_Symbol, _Period, 1); + RSI_Handle = iRSI(_Symbol, _Period, length, inp_price); + MFI_Handle = iMFI(_Symbol, _Period, length, VOLUME_TICK); + if(TR_Handle == INVALID_HANDLE || RSI_Handle == INVALID_HANDLE || MFI_Handle == INVALID_HANDLE) + return(INIT_FAILED); +//--- scope signal keys by symbol, timeframe, and indicator inputs + g_signal_prefix = StringFormat("AT_%s_%s_L%d_A%.1f_C%d_S%d", + _Symbol, + EnumToString(_Period), + length, + atr_multiplier, + (int)inp_change_calc, + (int)inp_signals); //--- initialization succeeded - return(INIT_SUCCEEDED); + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Custom indicator deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { + if(TR_Handle != INVALID_HANDLE) + IndicatorRelease(TR_Handle); + if(RSI_Handle != INVALID_HANDLE) + IndicatorRelease(RSI_Handle); + if(MFI_Handle != INVALID_HANDLE) + IndicatorRelease(MFI_Handle); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | @@ -196,15 +219,18 @@ int OnCalculate(const int rates_total, Sell_Signal[i] = inp_signals == No ? EMPTY_VALUE : Arrows[i] == 1.0 && Arrows[i - 1] == -1.0 ? Offset[i] : EMPTY_VALUE; } } -//--- write signals to GlobalVariable for Mt5Bridge - int last = rates_total - 1; - GlobalVariableSet("AT_Alpha_" + _Symbol, Alpha[last]); - GlobalVariableSet("AT_Offset_" + _Symbol, Offset[last]); - GlobalVariableSet("AT_Trend_" + _Symbol, Arrows[last]); - GlobalVariableSet("AT_Buy_" + _Symbol, Buy_Signal_Calc[last]); - GlobalVariableSet("AT_Sell_" + _Symbol, Sell_Signal_Calc[last]); +//--- write only closed-bar signals to GlobalVariable for Mt5Bridge + int last_closed = rates_total - 2; + if(last_closed > length) + { + GlobalVariableSet(g_signal_prefix + "_Alpha", Alpha[last_closed]); + GlobalVariableSet(g_signal_prefix + "_Offset", Offset[last_closed]); + GlobalVariableSet(g_signal_prefix + "_Trend", Arrows[last_closed]); + GlobalVariableSet(g_signal_prefix + "_Buy", Buy_Signal_Calc[last_closed]); + GlobalVariableSet(g_signal_prefix + "_Sell", Sell_Signal_Calc[last_closed]); + } //--- return value of prev_calculated for next call - return(rates_total); + return(rates_total); } //+------------------------------------------------------------------+ //| Function: Simple Moving Average (SMA) | @@ -223,9 +249,9 @@ double fSMA(const int _position, const int _period, const double &_input[]) //+------------------------------------------------------------------+ double fBarsSince(const int _position, const double &_input[]) { - int result = 0; - while(_input[_position - result] == 0.0) - result++; - return(result); + int result = 0; + while(_position - result >= 0 && _input[_position - result] == 0.0) + result++; + return(result); } -//+------------------------------------------------------------------+ \ No newline at end of file +//+------------------------------------------------------------------+ diff --git a/Program.cs b/Program.cs index 02890b4..603f9aa 100644 --- a/Program.cs +++ b/Program.cs @@ -1,6 +1,7 @@ using MtApi5; using Microsoft.AspNetCore.Mvc; using System.Reflection; +using System.Globalization; var builder = WebApplication.CreateBuilder(args); builder.WebHost.UseUrls("http://0.0.0.0:8080"); @@ -28,9 +29,30 @@ builder.Services.AddSingleton(sp => } return client; }); +builder.Services.AddSingleton(new SemaphoreSlim(1, 1)); builder.Services.AddHostedService(); var app = builder.Build(); +app.UseExceptionHandler(errorApp => +{ + errorApp.Run(async context => + { + var logger = context.RequestServices.GetRequiredService().CreateLogger("GlobalExceptionHandler"); + var exception = context.Features.Get()?.Error; + if (exception != null) + { + logger.LogError(exception, "Unhandled exception while processing {Method} {Path}", context.Request.Method, context.Request.Path); + } + + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsJsonAsync(new + { + detail = "The bridge could not process the request.", + status = StatusCodes.Status500InternalServerError + }); + }); +}); + // API Key 认证 const string API_KEY = "UiHMqtaYLZzwBdcuS4RFmEGhgDO8N2eI"; app.Use(async (context, next) => @@ -46,42 +68,63 @@ app.Use(async (context, next) => await next(); }); -app.MapGet("/health", (MtApi5Client mt) => +app.MapGet("/health", async (MtApi5Client mt, SemaphoreSlim gate) => { - 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 + await gate.WaitAsync(); + try { - 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" - }; + var connected = mt.ConnectionState == Mt5ConnectionState.Connected; + return connected + ? Results.Ok(new { status = "healthy", mt5_connected = true, mt5_version = "unknown", api_version = "1.0.0" }) + : Results.Json(new { status = "disconnected", mt5_connected = false, mt5_version = "unknown", api_version = "1.0.0" }, statusCode: StatusCodes.Status503ServiceUnavailable); + } + finally + { + gate.Release(); + } }); -app.MapGet("/symbols/{symbol}", (string symbol, MtApi5Client mt) => +app.MapGet("/account", async (MtApi5Client mt, SemaphoreSlim gate) => { + await gate.WaitAsync(); + try + { + 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 Results.Ok(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" + }); + } + finally + { + gate.Release(); + } +}); + +app.MapGet("/symbols/{symbol}", async (string symbol, MtApi5Client mt, SemaphoreSlim gate, ILoggerFactory loggerFactory) => +{ + if (string.IsNullOrWhiteSpace(symbol)) return Results.BadRequest(new { detail = "Symbol is required." }); + + await gate.WaitAsync(); try { var digits = (int)mt.SymbolInfoInteger(symbol, ENUM_SYMBOL_INFO_INTEGER.SYMBOL_DIGITS); @@ -101,7 +144,7 @@ app.MapGet("/symbols/{symbol}", (string symbol, MtApi5Client mt) => 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 + return Results.Ok(new { data = new[] { @@ -109,57 +152,65 @@ app.MapGet("/symbols/{symbol}", (string symbol, MtApi5Client mt) => }, count = 1, format = "json" - }); + }); } catch (Exception ex) { - Console.WriteLine($"ERROR /symbols/{symbol}: {ex.Message}"); - return Results.Problem($"Error: {ex.Message}"); + loggerFactory.CreateLogger("SymbolsEndpoint").LogError(ex, "Failed to read symbol info for {Symbol}", symbol); + return Results.Problem("Failed to read symbol info.", statusCode: StatusCodes.Status502BadGateway); + } + finally + { + gate.Release(); } }); -app.MapGet("/symbols/{symbol}/tick", (string symbol, MtApi5Client mt) => +app.MapGet("/symbols/{symbol}/tick", async (string symbol, MtApi5Client mt, SemaphoreSlim gate) => { - var tick = mt.SymbolInfoTick(symbol); - if (tick == null) return Results.NotFound(new { detail = $"Tick for {symbol} not found" }); + if (string.IsNullOrWhiteSpace(symbol)) return Results.BadRequest(new { detail = "Symbol is required." }); - 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) => -{ + await gate.WaitAsync(); 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 - }; + 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" + }); + } + finally + { + gate.Release(); + } +}); + +app.MapGet("/rates/from-pos", async (string symbol, string timeframe, int start_pos, int count, MtApi5Client mt, SemaphoreSlim gate, ILoggerFactory loggerFactory) => +{ + if (string.IsNullOrWhiteSpace(symbol)) return Results.BadRequest(new { detail = "Symbol is required." }); + if (!TryParseTimeframe(timeframe, out var tf)) return Results.BadRequest(new { detail = $"Unsupported timeframe '{timeframe}'." }); + if (start_pos < 0) return Results.BadRequest(new { detail = "start_pos must be greater than or equal to 0." }); + if (count <= 0 || count > 10000) return Results.BadRequest(new { detail = "count must be between 1 and 10000." }); + + await gate.WaitAsync(); + try + { const int chunkSize = 1000; var allRates = new List(); int remaining = count; @@ -200,151 +251,243 @@ app.MapGet("/rates/from-pos", (string symbol, string timeframe, int start_pos, i } catch (Exception ex) { - Console.WriteLine($"ERROR /rates/from-pos: {ex.Message}\n{ex.StackTrace}"); - return Results.Problem($"Error: {ex.Message}"); + loggerFactory.CreateLogger("RatesEndpoint").LogError(ex, "Failed to read rates for {Symbol} {Timeframe}", symbol, timeframe); + return Results.Problem("Failed to read rates.", statusCode: StatusCodes.Status502BadGateway); } -}); - -app.MapGet("/positions", (MtApi5Client mt) => -{ - var total = mt.PositionsTotal(); - var positions = new List(); - for (int i = 0; i < total; i++) + finally { - 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) - }); + gate.Release(); } - return new { data = positions, count = positions.Count, format = "json" }; }); -app.MapGet("/orders", (string? symbol, MtApi5Client mt) => +app.MapGet("/positions", async (MtApi5Client mt, SemaphoreSlim gate) => { - var total = mt.OrdersTotal(); - var orders = new List(); - for (int i = 0; i < total; i++) + await gate.WaitAsync(); + try { - 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 + var total = mt.PositionsTotal(); + var positions = new List(); + for (int i = 0; i < total; i++) { - 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) - }); + 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 Results.Ok(new { data = positions, count = positions.Count, format = "json" }); + } + finally + { + gate.Release(); } - return new { data = orders, count = orders.Count, format = "json" }; }); -app.MapPost("/order/check", (TradeRequestDto body, MtApi5Client mt) => +app.MapGet("/orders", async (string? symbol, MtApi5Client mt, SemaphoreSlim gate) => { + await gate.WaitAsync(); + try + { + 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 (!string.IsNullOrWhiteSpace(symbol) && 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 Results.Ok(new { data = orders, count = orders.Count, format = "json" }); + } + finally + { + gate.Release(); + } +}); + +app.MapPost("/order/check", async (TradeRequestDto body, MtApi5Client mt, SemaphoreSlim gate) => +{ + var validationError = ValidateTradeRequest(body); + if (validationError != null) return Results.BadRequest(new { detail = validationError }); + 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" }; + await gate.WaitAsync(); + try + { + mt.OrderCheck(request, out MqlTradeCheckResult? checkResult); + if (checkResult == null) return Results.Ok(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 Results.Ok(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" }); + } + finally + { + gate.Release(); + } }); -app.MapPost("/order/send", (TradeRequestBody body, MtApi5Client mt) => +app.MapPost("/order/send", async (TradeRequestBody body, MtApi5Client mt, SemaphoreSlim gate) => { + var validationError = ValidateTradeRequest(body.request); + if (validationError != null) return Results.BadRequest(new { detail = validationError }); + 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" }; + await gate.WaitAsync(); + try + { + mt.OrderSend(request, out MqlTradeResult? result); + if (result == null) return Results.Ok(new { data = new { retcode = 0u, order = 0ul, comment = "Send failed" }, count = 1, format = "json" }); + return Results.Ok(new { data = new { retcode = result.Retcode, order = result.Order, comment = result.Comment ?? "" }, count = 1, format = "json" }); + } + finally + { + gate.Release(); + } }); -app.MapGet("/history/deals", (string date_from, string date_to, string? symbol, MtApi5Client mt) => +app.MapGet("/history/deals", async (string date_from, string date_to, string? symbol, MtApi5Client mt, SemaphoreSlim gate) => { - 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++) + if (!TryParseClientDate(date_from, out var fromDt) || !TryParseClientDate(date_to, out var toDt)) + return Results.BadRequest(new { detail = "date_from and date_to must be valid ISO-8601 or yyyy-MM-dd values." }); + if (fromDt > toDt) + return Results.BadRequest(new { detail = "date_from must be earlier than or equal to date_to." }); + + await gate.WaitAsync(); + try { - 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 + mt.HistorySelect(fromDt, toDt); + var total = mt.HistoryDealsTotal(); + var dealList = new List(); + for (int i = 0; i < total; i++) { - 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) ?? "" - }); + var ticket = mt.HistoryDealGetTicket(i); + if (ticket == 0) continue; + var dealSymbol = mt.HistoryDealGetString(ticket, ENUM_DEAL_PROPERTY_STRING.DEAL_SYMBOL); + if (!string.IsNullOrWhiteSpace(symbol) && 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 Results.Ok(new { data = dealList, count = dealList.Count, format = "json" }); } - return new { data = dealList, count = dealList.Count, format = "json" }; -}); - -app.MapGet("/gvar", (HttpContext context) => -{ - var mt = context.RequestServices.GetRequiredService(); - var total = mt.GlobalVariablesTotal(); - var vars = new List(); - for (int i = 0; i < total; i++) + finally { - var name = mt.GlobalVariableName(i) ?? ""; - var value = mt.GlobalVariableGet(name); - vars.Add(new { name, value }); + gate.Release(); } - return Results.Ok(new { data = vars, count = vars.Count, format = "json" }); }); -app.MapGet("/gvar/{name}", (HttpContext context, string name) => +app.MapGet("/gvar", async (HttpContext context, SemaphoreSlim gate) => { var mt = context.RequestServices.GetRequiredService(); - if (!mt.GlobalVariableCheck(name)) - return Results.NotFound(new { detail = $"GlobalVariable '{name}' not found" }); - var value = mt.GlobalVariableGet(name); - return Results.Ok(new { name, value }); + await gate.WaitAsync(); + try + { + var total = mt.GlobalVariablesTotal(); + var vars = new List(); + for (int i = 0; i < total; i++) + { + var name = mt.GlobalVariableName(i) ?? ""; + var value = mt.GlobalVariableGet(name); + vars.Add(new { name, value }); + } + return Results.Ok(new { data = vars, count = vars.Count, format = "json" }); + } + finally + { + gate.Release(); + } }); -app.MapPost("/gvar/{name}", async (HttpContext context, string name) => +app.MapGet("/gvar/{name}", async (HttpContext context, string name, SemaphoreSlim gate) => { + if (string.IsNullOrWhiteSpace(name)) return Results.BadRequest(new { detail = "Global variable name is required." }); + + var mt = context.RequestServices.GetRequiredService(); + await gate.WaitAsync(); + try + { + if (!mt.GlobalVariableCheck(name)) + return Results.NotFound(new { detail = $"GlobalVariable '{name}' not found" }); + var value = mt.GlobalVariableGet(name); + return Results.Ok(new { name, value }); + } + finally + { + gate.Release(); + } +}); + +app.MapPost("/gvar/{name}", async (HttpContext context, string name, SemaphoreSlim gate) => +{ + if (string.IsNullOrWhiteSpace(name)) return Results.BadRequest(new { detail = "Global variable name is required." }); + var mt = context.RequestServices.GetRequiredService(); var body = await context.Request.ReadFromJsonAsync(); - if (body == null) return Results.BadRequest("Invalid body"); - mt.GlobalVariableSet(name, body.value); - return Results.Ok(new { name, value = body.value }); + if (body == null || double.IsNaN(body.value) || double.IsInfinity(body.value)) return Results.BadRequest(new { detail = "Invalid body." }); + + await gate.WaitAsync(); + try + { + mt.GlobalVariableSet(name, body.value); + return Results.Ok(new { name, value = body.value }); + } + finally + { + gate.Release(); + } }); -app.MapDelete("/gvar/{name}", (HttpContext context, string name) => +app.MapDelete("/gvar/{name}", async (HttpContext context, string name, SemaphoreSlim gate) => { + if (string.IsNullOrWhiteSpace(name)) return Results.BadRequest(new { detail = "Global variable name is required." }); + var mt = context.RequestServices.GetRequiredService(); - if (!mt.GlobalVariableCheck(name)) - return Results.NotFound(new { detail = $"GlobalVariable '{name}' not found" }); - mt.GlobalVariableDel(name); - return Results.Ok(new { deleted = name }); + await gate.WaitAsync(); + try + { + if (!mt.GlobalVariableCheck(name)) + return Results.NotFound(new { detail = $"GlobalVariable '{name}' not found" }); + mt.GlobalVariableDel(name); + return Results.Ok(new { deleted = name }); + } + finally + { + gate.Release(); + } }); app.Run(); @@ -354,6 +497,51 @@ DateTime MtTimeToDateTime(long mtTime) return new DateTime(1970, 1, 1).AddSeconds(mtTime); } +bool TryParseTimeframe(string timeframe, out ENUM_TIMEFRAMES parsed) +{ + parsed = 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, + _ => default + }; + + return timeframe is "TIMEFRAME_M1" or "TIMEFRAME_M5" or "TIMEFRAME_M15" or "TIMEFRAME_M30" or "TIMEFRAME_H1" or "TIMEFRAME_H4" or "TIMEFRAME_D1"; +} + +bool TryParseClientDate(string value, out DateTime parsed) +{ + return DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsed) + || DateTime.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsed); +} + +string? ValidateTradeRequest(TradeRequestDto request) +{ + if (!Enum.IsDefined(typeof(ENUM_TRADE_REQUEST_ACTIONS), (int)request.action)) + return "action is invalid."; + if (string.IsNullOrWhiteSpace(request.symbol)) + return "symbol is required."; + if (request.volume is not null && (!double.IsFinite(request.volume.Value) || request.volume.Value <= 0)) + return "volume must be a positive number."; + if (request.price is not null && (!double.IsFinite(request.price.Value) || request.price.Value < 0)) + return "price must be a non-negative number."; + if (request.sl is not null && (!double.IsFinite(request.sl.Value) || request.sl.Value < 0)) + return "sl must be a non-negative number."; + if (request.tp is not null && (!double.IsFinite(request.tp.Value) || request.tp.Value < 0)) + return "tp must be a non-negative number."; + if (request.order_type is not null && !Enum.IsDefined(typeof(ENUM_ORDER_TYPE), (int)request.order_type.Value)) + return "order_type is invalid."; + if (request.deviation is not null && request.deviation.Value > 100000) + return "deviation is unreasonably large."; + + return null; +} + MqlTradeRequest ToMqlTradeRequest(TradeRequestDto r) { return new MqlTradeRequest @@ -403,46 +591,80 @@ class MtConnectionService : BackgroundService { private readonly MtApi5Client _client; private readonly ILogger _logger; + private readonly TimeSpan _connectTimeout; + private const int Port = 8228; public MtConnectionService(MtApi5Client client, ILogger logger) { _client = client; _logger = logger; + _connectTimeout = TimeSpan.FromSeconds(15); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - var tcs = new TaskCompletionSource(); - _client.ConnectionStateChanged += (s, e) => + while (!stoppingToken.IsCancellationRequested) + { + if (_client.ConnectionState != Mt5ConnectionState.Connected) + { + var firstConnect = _client.ConnectionState == Mt5ConnectionState.Disconnected; + _logger.LogWarning(firstConnect + ? "Connecting to MT5 via MtApi5 on port {Port}..." + : "MT5 disconnected, attempting reconnect on port {Port}...", Port); + + var connected = await TryConnectAsync(stoppingToken); + if (!connected) + { + _logger.LogError("MT5 is not connected. Ensure MtApi5 EA is loaded and listening on port {Port}.", Port); + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + continue; + } + + _logger.LogInformation(firstConnect + ? "MT5 Bridge ready on http://localhost:8080" + : "MT5 reconnected."); + } + + await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); + } + } + + private async Task TryConnectAsync(CancellationToken stoppingToken) + { + if (_client.ConnectionState == Mt5ConnectionState.Connected) + { + return true; + } + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + EventHandler? handler = null; + handler = (_, 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); + { + tcs.TrySetResult(e.Status); + } }; - _logger.LogInformation("Connecting to MT5 via MtApi5 on port 8228..."); - _client.BeginConnect(8228); - await tcs.Task; - - if (_client.ConnectionState != Mt5ConnectionState.Connected) + _client.ConnectionStateChanged += handler; + try { - _logger.LogError("Failed to connect to MT5. Ensure MtApi5 EA is loaded in MT5."); - return; + _client.BeginConnect(Port); + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); + timeoutCts.CancelAfter(_connectTimeout); + using var registration = timeoutCts.Token.Register(() => tcs.TrySetCanceled(timeoutCts.Token)); + var status = await tcs.Task; + return status == Mt5ConnectionState.Connected; } - - _logger.LogInformation("MT5 Bridge ready on http://localhost:8080"); - while (!stoppingToken.IsCancellationRequested) + catch (OperationCanceledException) when (!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."); - } + _logger.LogWarning("Timed out waiting {TimeoutSeconds}s for MT5 connection state change.", _connectTimeout.TotalSeconds); + return false; + } + finally + { + _client.ConnectionStateChanged -= handler; } } -} \ No newline at end of file +} diff --git a/README.md.md b/README.md.md index 3e1d77e..ae7180e 100644 --- a/README.md.md +++ b/README.md.md @@ -38,10 +38,10 @@ ```powershell cd Mt5Bridge -dotnet build -c Debug +dotnet build -c Release ``` -编译产物在 `bin\Debug\net8.0\`,包含以下文件: +编译产物在 `bin\Release\net8.0\`,包含以下文件: | 文件 | 说明 | |------|------| @@ -77,7 +77,7 @@ dotnet --list-runtimes ### 2. 部署文件 -将 `bin\Debug\net8.0\` 整个文件夹复制到目标主机,例如 `C:\Mt5Bridge\`。 +将 `bin\Release\net8.0\` 整个文件夹复制到目标主机,例如 `C:\Mt5Bridge\`。 ### 3. 安装 MtApi5 @@ -129,8 +129,10 @@ Bridge 作为后台服务运行时,文件被锁定无法直接覆盖。需要 # 阻止任务计划再次触发 Stop-ScheduledTask -TaskName "Mt5Bridge" -# 强制结束当前进程 -taskkill /f /im dotnet.exe +# 仅结束当前 Bridge 进程 +Get-CimInstance Win32_Process | + Where-Object { $_.Name -eq "dotnet.exe" -and $_.CommandLine -like "*Mt5Bridge.dll*" } | + Invoke-CimMethod -MethodName Terminate # 现在可以覆盖 Mt5Bridge.dll 了 @@ -138,6 +140,12 @@ taskkill /f /im dotnet.exe Start-ScheduledTask -TaskName "Mt5Bridge" ``` +说明: + +- 仅更新 `Mt5Bridge.dll` / `Mt5Bridge.exe` 这一侧,就会影响 HTTP API、健康检查、参数校验、错误处理、重连逻辑等桥接服务行为。 +- `Alpha Trend.ex5` 不属于桥接服务本体,不替换也不会影响 `/health`、`/account`、`/symbols`、`/rates/from-pos`、`/positions`、`/orders`、`/history/deals`、`/gvar`、`/order/check`、`/order/send` 这些 API 的正常工作。 +- 只有在你需要新版指标导出逻辑时,才需要同步替换 `Alpha Trend.ex5`。新版指标变化包括:只输出已收盘 K 线信号,以及使用带周期/参数作用域的 GlobalVariable 名称。 + --- ## 四、端口映射(公网访问) @@ -230,6 +238,8 @@ GET /health } ``` +当 MT5 断开时,接口会返回 `503 Service Unavailable`。 + ### 账户信息 ``` @@ -255,6 +265,21 @@ GET /account | trade_allowed | bool | 是否允许交易 | | trade_expert | bool | 是否允许 EA 交易 | +实际响应结构: + +```json +{ + "data": [ + { + "login": 12345678, + "balance": 10000.0 + } + ], + "count": 1, + "format": "json" +} +``` + ### 品种信息 ``` @@ -463,12 +488,25 @@ Body: {"value": 75.5} DELETE /gvar/{name} ``` -**MQL5 侧(指标末尾加一行):** +**MQL5 侧(仅在你要升级指标信号导出逻辑时才需要修改):** ```mql5 -int last = rates_total - 1; -GlobalVariableSet("MY_SIGNAL_" + _Symbol, Alpha[last]); +int last_closed = rates_total - 2; +string key = StringFormat("MY_SIGNAL_%s_%s", _Symbol, EnumToString(_Period)); +GlobalVariableSet(key, Alpha[last_closed]); ``` +如果你没有替换新版 `Alpha Trend.ex5`,远程 `/gvar` 中仍会看到旧键名,例如: + +```text +AT_Alpha_XAUUSDc +AT_Buy_XAUUSDc +AT_Offset_XAUUSDc +AT_Sell_XAUUSDc +AT_Trend_XAUUSDc +``` + +这不影响桥接 API 本身,只表示 MT5 端仍在运行旧版指标。 + **Python 侧读取:** ```python resp = requests.get(f"{BRIDGE}/gvar/MY_SIGNAL_XAUUSDc", headers=HEADERS) @@ -577,7 +615,7 @@ curl -X POST http://IP:端口/order/send \ | 从哪里 | 复制什么 | 放到哪里 | |--------|---------|---------| -| 本地 `bin\Debug\net8.0\` | 整个文件夹 | 新主机 `C:\Mt5Bridge\` | +| 本地 `bin\Release\net8.0\` | 整个文件夹 | 新主机 `C:\Mt5Bridge\` | | MtApi5 安装包 | 运行 MSI 安装 | 新主机 | | MtApi5 项目 | `MtApi5.ex5` | 新主机 MT5 的 `MQL5\Experts\` | @@ -585,7 +623,7 @@ curl -X POST http://IP:端口/order/send \ 1. 安装 .NET 8 ASP.NET Core Runtime 2. 运行 MtApi5 MSI 安装包 -3. 复制 `bin\Debug\net8.0\` 到 `C:\Mt5Bridge\` +3. 复制 `bin\Release\net8.0\` 到 `C:\Mt5Bridge\` 4. 复制 `MtApi5.ex5` 到 MT5 Experts 目录 5. 在 MT5 图表上挂载 MtApi5 EA 6. 配置云服务商端口映射(如需要外网访问) @@ -623,4 +661,4 @@ Stop-ScheduledTask -TaskName "Mt5Bridge" 2. **端口号不要用默认的**:使用 13485-13489 范围内的随机端口 3. **防火墙白名单**:如果只有固定 IP 访问,在云服务商安全组里限制来源 IP 4. **不要用 HTTP 明文传输敏感数据**:可配合 Nginx Proxy Manager 加 HTTPS -5. **定期更换 API Key**:修改 `Program.cs` 中的 Key 后重新编译部署 \ No newline at end of file +5. **定期更换 API Key**:修改 `Program.cs` 中的 Key 后重新编译部署 diff --git a/bin/Debug/net8.0/Mt5Bridge.deps.json b/bin/Debug/net8.0/Mt5Bridge.deps.json deleted file mode 100644 index b1b55d0..0000000 --- a/bin/Debug/net8.0/Mt5Bridge.deps.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETCoreApp,Version=v8.0", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v8.0": { - "Mt5Bridge/1.0.0": { - "dependencies": { - "MtApi5": "2.0.0.0", - "MtClient": "1.0.0.0", - "Newtonsoft.Json": "13.0.0.0" - }, - "runtime": { - "Mt5Bridge.dll": {} - } - }, - "MtApi5/2.0.0.0": { - "runtime": { - "MtApi5.dll": { - "assemblyVersion": "2.0.0.0", - "fileVersion": "2.0.0.0" - } - } - }, - "MtClient/1.0.0.0": { - "runtime": { - "MtClient.dll": { - "assemblyVersion": "1.0.0.0", - "fileVersion": "1.0.0.0" - } - } - }, - "Newtonsoft.Json/13.0.0.0": { - "runtime": { - "Newtonsoft.Json.dll": { - "assemblyVersion": "13.0.0.0", - "fileVersion": "13.0.3.27908" - } - } - } - } - }, - "libraries": { - "Mt5Bridge/1.0.0": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "MtApi5/2.0.0.0": { - "type": "reference", - "serviceable": false, - "sha512": "" - }, - "MtClient/1.0.0.0": { - "type": "reference", - "serviceable": false, - "sha512": "" - }, - "Newtonsoft.Json/13.0.0.0": { - "type": "reference", - "serviceable": false, - "sha512": "" - } - } -} \ No newline at end of file diff --git a/bin/Debug/net8.0/Mt5Bridge.dll b/bin/Debug/net8.0/Mt5Bridge.dll deleted file mode 100644 index b30033c..0000000 Binary files a/bin/Debug/net8.0/Mt5Bridge.dll and /dev/null differ diff --git a/bin/Debug/net8.0/Mt5Bridge.exe b/bin/Debug/net8.0/Mt5Bridge.exe deleted file mode 100644 index 5ab86cb..0000000 Binary files a/bin/Debug/net8.0/Mt5Bridge.exe and /dev/null differ diff --git a/bin/Debug/net8.0/Mt5Bridge.pdb b/bin/Debug/net8.0/Mt5Bridge.pdb deleted file mode 100644 index b6a0e8f..0000000 Binary files a/bin/Debug/net8.0/Mt5Bridge.pdb and /dev/null differ diff --git a/bin/Debug/net8.0/Mt5Bridge.runtimeconfig.json b/bin/Debug/net8.0/Mt5Bridge.runtimeconfig.json deleted file mode 100644 index 5e604c7..0000000 --- a/bin/Debug/net8.0/Mt5Bridge.runtimeconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "runtimeOptions": { - "tfm": "net8.0", - "frameworks": [ - { - "name": "Microsoft.NETCore.App", - "version": "8.0.0" - }, - { - "name": "Microsoft.AspNetCore.App", - "version": "8.0.0" - } - ], - "configProperties": { - "System.GC.Server": true, - "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false - } - } -} \ No newline at end of file diff --git a/bin/Debug/net8.0/MtApi5.dll b/bin/Debug/net8.0/MtApi5.dll deleted file mode 100644 index 44f465d..0000000 Binary files a/bin/Debug/net8.0/MtApi5.dll and /dev/null differ diff --git a/bin/Debug/net8.0/MtClient.dll b/bin/Debug/net8.0/MtClient.dll deleted file mode 100644 index 94d0277..0000000 Binary files a/bin/Debug/net8.0/MtClient.dll and /dev/null differ diff --git a/bin/Debug/net8.0/Newtonsoft.Json.dll b/bin/Debug/net8.0/Newtonsoft.Json.dll deleted file mode 100644 index d035c38..0000000 Binary files a/bin/Debug/net8.0/Newtonsoft.Json.dll and /dev/null differ