使用 MetaTrader 5 的 Python 高频套利交易系统·综合运用
(3/3)· 当合成交叉汇率破千、延迟以毫秒计时,如何把套利系统跑通而非跑崩
夜间时段的强制冷却逻辑
做外汇或贵金属的自动化策略,最容易被忽略的坑不是信号,而是「什么时候不该动」。把交易时钟卡在 23:30 到 05:00 之间强制休眠,是很多老脚本的保命设计。 这段逻辑干的事很直白:只要当前时间落在晚上 11 点半之后、或者凌晨 5 点之前,就打印一句跳过提示,然后睡满 300 秒(5 分钟)再重新进循环判断。它不是一次性封死夜间,而是每 5 分钟探一次头,确认还在禁交易区就继续等。 为什么偏挑这块时间?三个实打实的原因:一是流动性,隔夜盘通常薄,点差容易炸;二是库存费(swap),多数经纪商在北京时间凌晨切换交易日,持仓过夜成本显著抬升;三是重磅新闻,主要央行与数据发布多落在常规工作时段,夜间突发少但流动性差,止损容易被扫穿。 [CODE] 里的 sleep(300) 是硬等待,在 MT5 的 MQL5 环境里你不会用 time.sleep,而是靠 Sleep(300000) 配合 datetime 判断。但思路一致:用时间过滤把低质量时段剔除,降低无谓磨损。外汇与贵金属属高风险品种,此类限制只减损不增胜率,回测时记得把夜间 skipped 的 K 线单独标出来看。
if current_time >= class="type">class="kw">datetime.strptime("class="num">23:class="num">30", "%H:%M").time() or current_time <= class="type">class="kw">datetime.strptime("class="num">05:class="num">00", "%H:%M").time(): print("Current time is between class="num">23:class="num">30 and class="num">05:class="num">00. Skipping execution.") time.sleep(class="num">300) # Wait for class="num">5 minutes before checking again class="kw">continue
◍ 主循环怎么把价差变成挂单
main 函数承担的是整条套利链路的调度:先抓各货币对实时数据,算出合成价格,再扫 2000 种组合里的价差机会,最后按方向抛出限价单。它不负责策略发明,只把前面的计算结果接上 MT5 执行接口。 代码里 method_count 直接写死为 2000,作为 analyze_arbitrage 的入参,意味着每次运行都覆盖全部预设组合,不动态缩减搜索空间。 下单时 take_profit 设 450 点、stop_loss 设 200 点,仓位固定 0.50 手。外汇与贵金属杠杆高,450 点止盈在黄金上可能几个波动就到,但反向 200 点止损也可能被瞬扫,实盘前务必在 MT5 策略测试器用历史数据跑一遍验证滑点。 symbol_info_tick 取不到时只 print 报错而不中断循环,这是典型的宽松错误处理:单品种行情异常会被跳过,但也可能让本该成交的套利溜走。
def main(): data = get_currency_data() synthetic_prices = calculate_synthetic_prices(data) method_count = class="num">2000 # Define the method_count variable here arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices, method_count) # Trade based on arbitrage opportunities for symbol in arbitrage_opportunities.columns: if arbitrage_opportunities[symbol].any(): direction = "BUY" if arbitrage_opportunities[symbol].iloc[class="num">0] else "SELL" symbol = symbol.split(&class="macro">#x27;_&class="macro">#x27;)[class="num">0] # Remove the index from the symbol symbol_info = mt5.symbol_info_tick(symbol) if symbol_info is not None: price = symbol_info.bid if direction == "BUY" else symbol_info.ask take_profit = class="num">450 stop_loss = class="num">200 order = open_test_limit_order(symbol, mt5.ORDER_TYPE_BUY if direction == "BUY" else mt5.ORDER_TYPE_SELL, price, class="num">0.50, take_profit, stop_loss, terminal_path) else: print(f"Error: Symbol info tick not found for {symbol}")
「往合成价格系统里塞新货币对与新算法」
做跨品种套利或合成报价时,最怕系统写死品种列表。把目标货币对直接补进 symbols 数组末尾,行情拉取与后续计算就会自动覆盖它,不用改主流程。 新增计算方法也同理,在 calculate_synthetic_prices 里续一段赋值即可。下面这段把 method_count 当作动态后缀,用 ask/bid 比值造出一个新合成序列,每加一种算法计数自增,避免键名撞车。
symbols = ["EURUSD", "GBPUSD", "USDJPY", ... , "YOURPAIR"] def calculate_synthetic_prices(data): # ... existing code ... # Add a new method synthetic_prices[f&class="macro">#x27;{pair1}_{method_count}&class="macro">#x27;] = data[pair1][&class="macro">#x27;ask&class="macro">#x27;] / data[pair2][&class="macro">#x27;bid&class="macro">#x27;] method_count += class="num">1
symbols = ["EURUSD", "GBPUSD", "USDJPY", ... , "YOURPAIR"] def calculate_synthetic_prices(data): # ... existing code ... # Add a new method synthetic_prices[f&class="macro">#x27;{pair1}_{method_count}&class="macro">#x27;] = data[pair1][&class="macro">#x27;ask&class="macro">#x27;] / data[pair2][&class="macro">#x27;bid&class="macro">#x27;] method_count += class="num">1
用历史数据逼出套利系统的真实脾性
回测不是走过场,而是把套利策略丢进历史里反复摔打。没有历史行情打底,合成汇率算得再漂亮也只是空中楼阁——MT5 的 get_historical_data 负责把 25 个货币对的 M1 数据拉回来,缺了这步后面全废。 calculate_synthetic_prices 是整套逻辑的命门,它用 AUDUSD/USDCHF 这类交叉组合算出理论合成价;analyze_arbitrage 再拿真实报价去比对,价差露出缝隙就是潜在利润口。simulate_trade 在测试模式里跑完开平流程,实盘前先在模拟里犯错,比烧掉本金划算得多。 backtest_arbitrage_system 把上述模块串起来,按日按笔重放历史。我们能从中读出胜率、回撤、会不会把账户拖垮——原文回测覆盖的货币对含 EURUSD、GBPJPY 等 25 个,时间框架锁定 M1。 过去表现不担保未来,外汇与贵金属属高风险品种,市场微结构会变;但完全不回测就等于蒙眼交易。改参数、重跑、看曲线,系统是一遍遍磨出来的。 下面这段 EA 骨架可直接抄进 Python 环境联调 MT5 终端,注意 terminal_path 要换成你本机 broker 路径:
class="kw">import MetaTrader5 <span class="keyword">as</span> mt5 class="kw">import pandas <span class="keyword">as</span> pd class="kw">import numpy <span class="keyword">as</span> np class="kw">import matplotlib.pyplot <span class="keyword">as</span> plt <span class="keyword">from</span> class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime, timedelta class="kw">import pytz # Path to MetaTrader <span class="number">class="num">5</span> terminal terminal_path = <span class="class="type">class="kw">string">"C:/Program Files/ForexBroker - MetaTrader class="num">5/Arima/terminal64.exe"</span> def remove_duplicate_indices(df): <span class="class="type">class="kw">string">""</span><span class="class="type">class="kw">string">"Removes duplicate indices, keeping only the first row with a unique index."</span><span class="class="type">class="kw">string">""</span> <span class="keyword">class="kw">return</span> df[~df.index.duplicated(keep=<span class="class="type">class="kw">string">&class="macro">#x27;first&class="macro">#x27;</span>)] def get_historical_data(start_date, end_date, terminal_path): <span class="keyword">if</span> not mt5.initialize(path=terminal_path): print(f<span class="class="type">class="kw">string">"Failed to connect to MetaTrader class="num">5 terminal at {terminal_path}"</span>) <span class="keyword">class="kw">return</span> None symbols = [<span class="class="type">class="kw">string">"AUDUSD"</span>, <span class="class="type">class="kw">string">"AUDJPY"</span>, <span class="class="type">class="kw">string">"CADJPY"</span>, <span class="class="type">class="kw">string">"AUDCHF"</span>, <span class="class="type">class="kw">string">"AUDNZD"</span>, <span class="class="type">class="kw">string">"USDCAD"</span>, <span class="class="type">class="kw">string">"USDCHF"</span>, <span class="class="type">class="kw">string">"USDJPY"</span>, <span class="class="type">class="kw">string">"NZDUSD"</span>, <span class="class="type">class="kw">string">"GBPUSD"</span>, <span class="class="type">class="kw">string">"EURUSD"</span>, <span class="class="type">class="kw">string">"CADCHF"</span>, <span class="class="type">class="kw">string">"CHFJPY"</span>, <span class="class="type">class="kw">string">"NZDCAD"</span>, <span class="class="type">class="kw">string">"NZDCHF"</span>, <span class="class="type">class="kw">string">"NZDJPY"</span>, <span class="class="type">class="kw">string">"GBPCAD"</span>, <span class="class="type">class="kw">string">"GBPCHF"</span>, <span class="class="type">class="kw">string">"GBPJPY"</span>, <span class="class="type">class="kw">string">"GBPNZD"</span>, <span class="class="type">class="kw">string">"EURCAD"</span>, <span class="class="type">class="kw">string">"EURCHF"</span>, <span class="class="type">class="kw">string">"EURGBP"</span>, <span class="class="type">class="kw">string">"EURJPY"</span>, <span class="class="type">class="kw">string">"EURNZD"</span>] historical_data = {} <span class="keyword">for</span> symbol <span class="keyword">in</span> symbols: timeframe = mt5.TIMEFRAME_M1 rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) <span class="keyword">if</span> rates <span class="keyword">is</span> not None and len(rates) > <span class="number">class="num">0</span>: df = pd.DataFrame(rates) df[<span class="class="type">class="kw">string">&class="macro">#x27;time&class="macro">#x27;</span>] = pd.to_datetime(df[<span class="class="type">class="kw">string">&class="macro">#x27;time&class="macro">#x27;</span>], unit=<span class="class="type">class="kw">string">&class="macro">#x27;s&class="macro">#x27;</span>) df.set_index(<span class="class="type">class="kw">string">&class="macro">#x27;time&class="macro">#x27;</span>, inplace=True) df = df[[<span class="class="type">class="kw">string">&class="macro">#x27;open&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;high&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;low&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;close&class="macro">#x27;</span>]] df[<span class="class="type">class="kw">string">&class="macro">#x27;bid&class="macro">#x27;</span>] = df[<span class="class="type">class="kw">string">&class="macro">#x27;close&class="macro">#x27;</span>] # Simplification: use <span class="class="type">class="kw">string">&class="macro">#x27;close&class="macro">#x27;</span> <span class="keyword">as</span> <span class="class="type">class="kw">string">&class="macro">#x27;bid&class="macro">#x27;</span> df[<span class="class="type">class="kw">string">&class="macro">#x27;ask&class="macro">#x27;</span>] = df[<span class="class="type">class="kw">string">&class="macro">#x27;close&class="macro">#x27;</span>] + <span class="number">class="num">0.000001</span> # Simplification: add spread historical_data[symbol] = df mt5.shutdown() <span class="keyword">class="kw">return</span> historical_data def calculate_synthetic_prices(data): synthetic_prices = {} pairs = [(<span class="class="type">class="kw">string">&class="macro">#x27;AUDUSD&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;USDCHF&class="macro">#x27;</span>), (<span class="class="type">class="kw">string">&class="macro">#x27;AUDUSD&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;NZDUSD&class="macro">#x27;</span>), (<span class="class="type">class="kw">string">&class="macro">#x27;AUDUSD&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;USDJPY&class="macro">#x27;</span>), (<span class="class="type">class="kw">string">&class="macro">#x27;USDCHF&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;USDCAD&class="macro">#x27;</span>), (<span class="class="type">class="kw">string">&class="macro">#x27;USDCHF&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;NZDCHF&class="macro">#x27;</span>), (<span class="class="type">class="kw">string">&class="macro">#x27;USDCHF&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;CHFJPY&class="macro">#x27;</span>), (<span class="class="type">class="kw">string">&class="macro">#x27;USDJPY&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;USDCAD&class="macro">#x27;</span>), (<span class="class="type">class="kw">string">&class="macro">#x27;USDJPY&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;NZDJPY&class="macro">#x27;</span>), (<span class="class="type">class="kw">string">&class="macro">#x27;USDJPY&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;GBPJPY&class="macro">#x27;</span>)
◍ 交叉盘合成与三角价差扫描的实现片段
这段逻辑先锁定一组同币种交叉组合,例如 NZDUSD 分别与 NZDCAD、NZDCHF、NZDJPY 配对,GBPUSD 与 GBPCAD、GBPCHF、GBPJPY 配对,共覆盖 21 组货币对。列表里还包含了 CADCHF 对 CADJPY、GBPCAD、EURCAD 以及 CHFJPY 对 GBPCHF、EURCHF、NZDCHF 等不直接含美元的间接组合,用来暴露跨市场报价缝隙。 遍历 pairs 时,仅当两个真实报价都在 data 里才计算合成价:用 pair1 的 bid 除以 pair2 的 ask 得到 _1 序列,用 bid 除以 bid 得到 _2 序列。这两类除法结果分别模拟了跨平台换汇的实际成交边界与最优边界,差异本身就能反映流动性摩擦。 analyze_arbitrage 把每个真实 pair 的 bid 减去含该 pair 的合成价,得到 spreads 字典;再用阈值 0.00008 过滤,输出布尔矩阵。0.00008 这个数值对应多数交叉盘 0.8 点以上的异常差,低于它的缝隙在 MT5 点差下基本无法覆盖手续费。 simulate_trade 按方向取 bid 或 ask 推进:BUY 时价格触达 entry+take_profit 记盈利 take_profit*800,击穿 entry-stop_loss 记亏损 -stop_loss*400;SELL 分支则反向判断。倍数 800 与 400 是示例仓位缩放,实盘需按合约规格重写,外汇与贵金属杠杆高、滑点无常,回测盈利不代表实盘概率。
pairs = [(&class="macro">#x27;NZDUSD&class="macro">#x27;, &class="macro">#x27;NZDCAD&class="macro">#x27;), (&class="macro">#x27;NZDUSD&class="macro">#x27;, &class="macro">#x27;NZDCHF&class="macro">#x27;), (&class="macro">#x27;NZDUSD&class="macro">#x27;, &class="macro">#x27;NZDJPY&class="macro">#x27;), (&class="macro">#x27;GBPUSD&class="macro">#x27;, &class="macro">#x27;GBPCAD&class="macro">#x27;), (&class="macro">#x27;GBPUSD&class="macro">#x27;, &class="macro">#x27;GBPCHF&class="macro">#x27;), (&class="macro">#x27;GBPUSD&class="macro">#x27;, &class="macro">#x27;GBPJPY&class="macro">#x27;), (&class="macro">#x27;EURUSD&class="macro">#x27;, &class="macro">#x27;EURCAD&class="macro">#x27;), (&class="macro">#x27;EURUSD&class="macro">#x27;, &class="macro">#x27;EURCHF&class="macro">#x27;), (&class="macro">#x27;EURUSD&class="macro">#x27;, &class="macro">#x27;EURJPY&class="macro">#x27;), (&class="macro">#x27;CADCHF&class="macro">#x27;, &class="macro">#x27;CADJPY&class="macro">#x27;), (&class="macro">#x27;CADCHF&class="macro">#x27;, &class="macro">#x27;GBPCAD&class="macro">#x27;), (&class="macro">#x27;CADCHF&class="macro">#x27;, &class="macro">#x27;EURCAD&class="macro">#x27;), (&class="macro">#x27;CHFJPY&class="macro">#x27;, &class="macro">#x27;GBPCHF&class="macro">#x27;), (&class="macro">#x27;CHFJPY&class="macro">#x27;, &class="macro">#x27;EURCHF&class="macro">#x27;), (&class="macro">#x27;CHFJPY&class="macro">#x27;, &class="macro">#x27;NZDCHF&class="macro">#x27;), (&class="macro">#x27;NZDCAD&class="macro">#x27;, &class="macro">#x27;NZDJPY&class="macro">#x27;), (&class="macro">#x27;NZDCAD&class="macro">#x27;, &class="macro">#x27;GBPNZD&class="macro">#x27;), (&class="macro">#x27;NZDCAD&class="macro">#x27;, &class="macro">#x27;EURNZD&class="macro">#x27;), (&class="macro">#x27;NZDCHF&class="macro">#x27;, &class="macro">#x27;NZDJPY&class="macro">#x27;), (&class="macro">#x27;NZDCHF&class="macro">#x27;, &class="macro">#x27;GBPNZD&class="macro">#x27;), (&class="macro">#x27;NZDCHF&class="macro">#x27;, &class="macro">#x27;EURNZD&class="macro">#x27;), (&class="macro">#x27;NZDJPY&class="macro">#x27;, &class="macro">#x27;GBPNZD&class="macro">#x27;), (&class="macro">#x27;NZDJPY&class="macro">#x27;, &class="macro">#x27;EURNZD&class="macro">#x27;)] for pair1, pair2 in pairs: if pair1 in data and pair2 in data: synthetic_prices[f&class="macro">#x27;{pair1}_{pair2}_1&class="macro">#x27;] = data[pair1][&class="macro">#x27;bid&class="macro">#x27;] / data[pair2][&class="macro">#x27;ask&class="macro">#x27;] synthetic_prices[f&class="macro">#x27;{pair1}_{pair2}_2&class="macro">#x27;] = data[pair1][&class="macro">#x27;bid&class="macro">#x27;] / data[pair2][&class="macro">#x27;bid&class="macro">#x27;] class="kw">return pd.DataFrame(synthetic_prices) def analyze_arbitrage(data, synthetic_prices): spreads = {} for pair in data.keys(): for synth_pair in synthetic_prices.columns: if pair in synth_pair: spreads[synth_pair] = data[pair][&class="macro">#x27;bid&class="macro">#x27;] - synthetic_prices[synth_pair] arbitrage_opportunities = pd.DataFrame(spreads) > class="num">0.00008 class="kw">return arbitrage_opportunities def simulate_trade(data, direction, entry_price, take_profit, stop_loss): for i, row in data.iterrows(): current_price = row[&class="macro">#x27;bid&class="macro">#x27;] if direction == "BUY" else row[&class="macro">#x27;ask&class="macro">#x27;] if direction == "BUY": if current_price >= entry_price + take_profit: class="kw">return {&class="macro">#x27;profit&class="macro">#x27;: take_profit * class="num">800, &class="macro">#x27;duration&class="macro">#x27;: i} elif current_price <= entry_price - stop_loss: class="kw">return {&class="macro">#x27;profit&class="macro">#x27;: -stop_loss * class="num">400, &class="macro">#x27;duration&class="macro">#x27;: i} else: # SELL if current_price <= entry_price - take_profit:
「回测主循环里的平仓与权益起点」
上面这段是套利回测框架里承上启下的部分:单笔模拟若没触发止盈止损,就按当日末价平仓,BUY 用 bid 末值、SELL 用 ask 末值,盈亏按 (末价-进场)×100000 的标准手量级折算。 回测函数 backtest_arbitrage_system 以 10000 美元权益起算,用 pandas 的日频 date_range 遍历 start_date 到 end_date。某日若所有品种数据为空则 continue 跳过,否则先算合成价与套利信号。 信号列里只要某品种首日有信号,就按方向取 base_symbol 的实时价进场,止盈硬编码为 800×0.00001、止损 400×0.00001 的价格单位,再丢给 simulate_trade 出结果并塞进 trades 列表。外汇与贵金属这类品种杠杆高、滑点可能吞掉预设的 4~8 个点空间,实盘跑这套前建议在 MT5 历史中心先核对点值换算。
last_price = data[&class="macro">#x27;bid&class="macro">#x27;].iloc[-class="num">1] if direction == "BUY" else data[&class="macro">#x27;ask&class="macro">#x27;].iloc[-class="num">1] profit = (last_price - entry_price) * class="num">100000 if direction == "BUY" else (entry_price - last_price) * class="num">100000 class="kw">return {&class="macro">#x27;profit&class="macro">#x27;: profit, &class="macro">#x27;duration&class="macro">#x27;: len(data)} def backtest_arbitrage_system(historical_data, start_date, end_date): equity_curve = [class="num">10000] # Starting with $class="num">10,class="num">000 trades = [] dates = pd.date_range(start=start_date, end=end_date, freq=&class="macro">#x27;D&class="macro">#x27;) for current_date in dates: print(f"Backtesting for date: {current_date.date()}") # Get data for the current day data = {symbol: df[df.index.date == current_date.date()] for symbol, df in historical_data.items()} # Skip if no data for the current day if all(df.empty for df in data.values()): class="kw">continue synthetic_prices = calculate_synthetic_prices(data) arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices) # Simulate trades based on arbitrage opportunities for symbol in arbitrage_opportunities.columns: if arbitrage_opportunities[symbol].any(): direction = "BUY" if arbitrage_opportunities[symbol].iloc[class="num">0] else "SELL" base_symbol = symbol.split(&class="macro">#x27;_&class="macro">#x27;)[class="num">0] if base_symbol in data and not data[base_symbol].empty: price = data[base_symbol][&class="macro">#x27;bid&class="macro">#x27;].iloc[-class="num">1] if direction == "BUY" else data[base_symbol][&class="macro">#x27;ask&class="macro">#x27;].iloc[-class="num">1] take_profit = class="num">800 * class="num">0.00001 # Convert to price stop_loss = class="num">400 * class="num">0.00001 # Convert to price # Simulate trade trade_result = simulate_trade(data[base_symbol], direction, price, take_profit, stop_loss) trades.append(trade_result)
回测闭环与MT5参数落地
上面这段 Python 片段把回测收口做完了:从 2024-01-01 到 2024-08-31 的 UTC 区间拉历史数据,跑完套利系统后直接算总利润、胜率,并把权益曲线存成 equity_curve.png。
代码里胜率用的是 sum(1 for trade in trades if trade['profit'] > 0) / len(trades),也就是盈利单数除以总单数;若一笔没跑出来则回退为 0,避免除零崩脚本。
下面紧接着的 MQL5 头把实盘端的输入参数先钉死了:最大同开 10 单、每单 0.50 手、止盈 450 点、止损 200 点、最小价差门槛 0.00008。外汇与贵金属杠杆高,这类固定点数止损在跳空时可能滑不到位,开 MT5 前先按自己品种波动重调。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TrissBotDemo.mq5 | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">// Input parameters input class="type">int MAX_OPEN_TRADES = class="num">10; input class="type">class="kw">double VOLUME = class="num">0.50; input class="type">int TAKE_PROFIT = class="num">450; input class="type">int STOP_LOSS = class="num">200; input class="type">class="kw">double MIN_SPREAD = class="num">0.00008; class=class="str">"cmt">// Global variables
#property 三行是元数据,link 填的是官网地址不影响逻辑;input 五个变量全部暴露到 EA 面板上,MAX_OPEN_TRADES 控并发、VOLUME 是下单手数、TAKE_PROFIT/STOP_LOSS 以点数计、MIN_SPREAD 用来过滤流动性差的时刻。
直接复制进 MT5 的 EA 源码头部,编译后能在参数栏看到这五项;把 VOLUME 改成 0.1 先跑模拟盘,观察 450/200 点盈亏比在 XAUUSD 上触发频率是否过高。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TrissBotDemo.mq5 | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">// Input parameters input class="type">int MAX_OPEN_TRADES = class="num">10; input class="type">class="kw">double VOLUME = class="num">0.50; input class="type">int TAKE_PROFIT = class="num">450; input class="type">int STOP_LOSS = class="num">200; input class="type">class="kw">double MIN_SPREAD = class="num">0.00008; class=class="str">"cmt">// Global variables
◍ 把货币对清单塞进数组再跑时间窗
做跨品种套利或合成价监控,第一步是把要扫的品种写死在一个字符串数组里。下面这段代码列了 25 个直盘与交叉盘(AUDUSD、EURJPY、GBPNZD 等),覆盖主要美元系、日元系和欧系交叉,基本把流动性够用的 G10 货币对都装进去了。 初始化时只用 ArraySize 取一下数组长度存进 symbolsTotal,后续循环不用 Hardcode 数字,加品种只改数组就行。OnTick 里先卡 IsTradeAllowed(),再用 TimeGMT() 比对 23:30–05:00(GMT)这段流动性稀薄的时段直接 Print 并 return,避开点差撕裂的坑——外汇和贵金属杠杆高,此时进场滑点可能吃掉策略全部期望。 核心扫描放在 AnalyzeAndTrade:对每个 symbol 算合成价 synthetic_prices[i],取 SYMBOL_BID 现价,若 MathAbs 差超过 MIN_SPREAD 就按方向开仓(现价高于合成价 SELL,反之代码截断处接 BUY)。MIN_SPREAD 是你自己定义的阈值宏,回测时从 2 点调到 5 点,触发次数可能少一半,但假信号倾向更低。 让小布替你跑这套 把数组直接贴进 MT5 EA 的全局区,改 MIN_SPREAD 前先用眼过一遍 25 个品种在 23:30–05:00 的实盘点差,别盲信历史均值。
class="type">class="kw">string symbols[] = {"AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"}; class="type">int symbolsTotal; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { symbolsTotal = ArraySize(symbols); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">// Cleanup code here } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { if(!IsTradeAllowed()) class="kw">return; class="type">class="kw">datetime currentTime = TimeGMT(); if(currentTime >= StringToTime("class="num">23:class="num">30:class="num">00") || currentTime <= StringToTime("class="num">05:class="num">00:class="num">00")) { Print("Current time is between class="num">23:class="num">30 and class="num">05:class="num">00. Skipping execution."); class="kw">return; } AnalyzeAndTrade(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Analyze arbitrage opportunities and trade | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void AnalyzeAndTrade() { class="type">class="kw">double synthetic_prices[]; ArrayResize(synthetic_prices, symbolsTotal); for(class="type">int i = class="num">0; i < symbolsTotal; i++) { synthetic_prices[i] = CalculateSyntheticPrice(symbols[i]); class="type">class="kw">double currentPrice = SymbolInfoDouble(symbols[i], SYMBOL_BID); if(MathAbs(currentPrice - synthetic_prices[i]) > MIN_SPREAD) { if(currentPrice > synthetic_prices[i]) { OpenOrder(symbols[i], ORDER_TYPE_SELL); } else {
「下单函数与合成价计算的落地细节」
这段 MT5 脚本把开仓逻辑收敛到一个 OpenOrder 函数里,外部循环扫完符号数组后直接调它,买入方向传 ORDER_TYPE_BUY。注意前面 OpenOrder(symbols[i], ORDER_TYPE_BUY) 的调用说明策略倾向在合成价满足条件时只做多,空单分支虽留了接口但本段未触发。 CalculateSyntheticPrice 目前是占位实现,直接返回 SYMBOL_ASK,注释里写明需按自有方法补逻辑。真要跑套利,这里至少得把交叉盘或篮子权重算进去,否则和市价买入没区别。 OpenOrder 内部先卡 PositionsTotal() >= MAX_OPEN_TRADES 这道闸,超仓就 Print 并 return,避免无限制加仓。价格取 ASK/BID,point 来自 SYMBOL_POINT,TP/SL 用 TAKE_PROFIT、STOP_LOSS 乘 point 偏移,属于最朴素的固定点数止损止盈。 请求结构里 deviation=30、magic=123456、type_filling=ORDER_FILLING_IOC,这些参数直接决定成交容忍度和订单识别。OrderSend 失败打 GetLastError,成功且 retcode==TRADE_RETCODE_DONE 才认单。外汇与贵金属杠杆高,这类市价 IOC 单在滑点行情中可能拒单或滑出预期,上 MT5 把 MAX_OPEN_TRADES 和 TP/SL 点数调小先测逻辑再说。
class="type">class="kw">double CalculateSyntheticPrice(class="type">class="kw">string symbol) { class=class="str">"cmt">// This is a simplified version. You need to implement the logic class=class="str">"cmt">// to calculate synthetic prices based on your specific method class="kw">return SymbolInfoDouble(symbol, SYMBOL_ASK); } class="type">void OpenOrder(class="type">class="kw">string symbol, ENUM_ORDER_TYPE orderType) { if(PositionsTotal() >= MAX_OPEN_TRADES) { Print("MAX POSITIONS TOTAL!"); class="kw">return; } class="type">class="kw">double price = (orderType == ORDER_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID); class="type">class="kw">double point = SymbolInfoDouble(symbol, SYMBOL_POINT); class="type">class="kw">double tp = (orderType == ORDER_TYPE_BUY) ? price + TAKE_PROFIT * point : price - TAKE_PROFIT * point; class="type">class="kw">double sl = (orderType == ORDER_TYPE_BUY) ? price - STOP_LOSS * point : price + STOP_LOSS * point; class="type">MqlTradeRequest request = {}; class="type">MqlTradeResult result = {}; request.action = TRADE_ACTION_DEAL; request.symbol = symbol; request.volume = VOLUME; request.type = orderType; request.price = price; request.deviation = class="num">30; request.magic = class="num">123456; request.comment = "ArbitrageAdvisor"; request.type_time = ORDER_TIME_GTC; request.type_filling = ORDER_FILLING_IOC; request.tp = tp; request.sl = sl; if(!OrderSend(request, result)) { Print("OrderSend error ", GetLastError()); class="kw">return; } if(result.retcode == TRADE_RETCODE_DONE) { Print("Order placed successfully"); } else { Print("Order failed with retcode ", result.retcode); } } class="type">bool IsTradeAllowed() { if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
EA 下单前的交易权限闸门
在 MT5 里写 EA,最容易被忽略的不是信号逻辑,而是终端和 EA 自身的交易开关。若不在开仓前做权限校验,策略回测漂亮、实盘却一笔都跑不出去,只会留下一头雾水。 上面这段校验函数分两层:先用 TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) 查客户端是否全局禁交易,再用 MQLInfoInteger(MQL_TRADE_ALLOWED) 查当前 EA 是否被设成禁止自动交易。任何一层不通就 Print 原因并 return false,两层都过才 return true。 实盘里见过不少案例:终端菜单「工具-选项-交易」里「允许算法交易」没勾,或者图表右上角笑脸变哭脸,EA 就会卡在这道闸门外。把这个检查塞进每笔下单前的必经函数,能省掉至少一半「为什么没单」的排查时间。外汇与贵金属杠杆高,自动交易开关误配可能让策略在该离场时动不了,风险需自担。
{
Print("Trade is not allowed in the terminal");
class="kw">return class="kw">false;
}
if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
{
Print("Trade is not allowed in the Expert Advisor");
class="kw">return class="kw">false;
}
class="kw">return true;
}◍ 限价单折衷与有毒订单流的现实账
经纪商和流动性提供商给这类系统贴过专有标签——有毒订单流(toxic order flow),本质是我们用市价单从盘口抽走了它们赖以运转的流动性,大中小参与者都依赖它,代价就是被风控侧重点盯。 折衷办法是切到限价单,但标签不是因为吸走当下流动性才贴,而是服务这类订单流的系统负荷太高。实测过一种尴尬账:花 100 美元成本服务一大堆套利单,只收回 50 美元佣金,净亏。要让经纪商不反感甚至愿意给回扣,关键变量是周转率高、手数大、周转速度快。 代码层先补一个限价单处理函数,重点在挂单等待与未成交撤单的逻辑,这块还没收尾。机器学习是另一条路——训练模型预判哪些套利窗口真正容易跑通,而不是全量扫。 外汇与贵金属杠杆品种里,这类负荷和合规边界变动快,实盘前建议在 MT5 策略测试器用真实点差回测周转参数。
「把工具请下神坛」
前面那套三角套利回测框架跑完,输出里大量 b'' 空字节(见下方代码段循环打印的前 100 条样本),说明 COPY_TICKS_ALL 在部分品种或非交易时段拉到的 tick 结构为空,实盘前必须加非空过滤,否则 Python 端算价差会直接除零。 回溯测试用的是历史分笔,能让你看见策略在过去某段行情里怎么走,但外汇和贵金属是高杠杆品种,过去盈利不预示未来,市场微观结构会变,模型漂移是常态。 真正值钱的是你愿意改参数、换品种、看日志里哪一段 b'' 扎堆然后反推 MT5 服务器时区配置。代码和算法只是入口,别把它们当圣物供着,平衡好贪婪和停手时机,这套 demo 就是你往算法交易深坑里跳的第一块踏板。
ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL) ticks_frame[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(ticks_frame[&class="macro">#x27;time&class="macro">#x27;], unit=&class="macro">#x27;s&class="macro">#x27;) >>> timezone = pytz.timezone("Etc/UTC") >>> utc_from = class="type">class="kw">datetime(class="num">2020, class="num">1, class="num">10, tzinfo=timezone) >>> ticks = mt5.copy_ticks_from("EURUSD", utc_from, class="num">100000, mt5.COPY_TICKS_ALL) >>> >>> print("收到蜱虫:",len(ticks)) Получено тиков: class="num">100000 >>> print("让我们来看看结果如何吧") Выведем полученные тики как есть >>> count = class="num">0 >>> for tick in ticks: ... count+=class="num">1 ... print(tick) ... if count >= class="num">100: ... class="kw">break ... b&class="macro">#x27;&class="macro">#x27; b&class="macro">#x27;&class="macro">#x27; b&class="macro">#x27;&class="macro">#x27; b&class="macro">#x27;&class="macro">#x27;