加固桥接服务并更新文档

This commit is contained in:
2026-07-04 22:42:26 +08:00
parent bb3aecc6ae
commit 148b7dd167
12 changed files with 541 additions and 325 deletions
+24 -8
View File
@@ -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`
Bridge 未运行或网络不通,先检查 `/health`
+43 -17
View File
@@ -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);
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
+425 -203
View File
@@ -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<MtApi5Client>(sp =>
}
return client;
});
builder.Services.AddSingleton(new SemaphoreSlim(1, 1));
builder.Services.AddHostedService<MtConnectionService>();
var app = builder.Build();
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("GlobalExceptionHandler");
var exception = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>()?.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<MqlRates>();
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<object>();
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<object>();
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<object>();
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<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 (!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<object>();
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<object>();
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<MtApi5Client>();
var total = mt.GlobalVariablesTotal();
var vars = new List<object>();
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<MtApi5Client>();
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<object>();
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<MtApi5Client>();
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<MtApi5Client>();
var body = await context.Request.ReadFromJsonAsync<GvarSetDto>();
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<MtApi5Client>();
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<MtConnectionService> _logger;
private readonly TimeSpan _connectTimeout;
private const int Port = 8228;
public MtConnectionService(MtApi5Client client, ILogger<MtConnectionService> logger)
{
_client = client;
_logger = logger;
_connectTimeout = TimeSpan.FromSeconds(15);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var tcs = new TaskCompletionSource<bool>();
_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<bool> TryConnectAsync(CancellationToken stoppingToken)
{
if (_client.ConnectionState == Mt5ConnectionState.Connected)
{
return true;
}
var tcs = new TaskCompletionSource<Mt5ConnectionState>(TaskCreationOptions.RunContinuationsAsynchronously);
EventHandler<Mt5ConnectionEventArgs>? 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<bool>();
_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;
}
}
}
}
+49 -11
View File
@@ -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 后重新编译部署
5. **定期更换 API Key**:修改 `Program.cs` 中的 Key 后重新编译部署
-67
View File
@@ -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": ""
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.