开发Python交易机器人(第三部分):实现基于模型的交易算法·进阶篇
用余额回撤自动砍仓位的逻辑
外汇与贵金属杠杆交易的高风险首先体现在权益回撤上:一天内账户余额跌超 2%,往往意味着持仓方向已短暂失控。与其手动降仓,不如让脚本每天拉一次账户信息,按回撤深度自动缩减开仓量。 具体规则很直接——单日余额回撤 >2% 时,每触发一次就将下次开仓体积减 0.01 手,并守住 0.01 手下限;若余额刷新历史峰值,体积回到初始 0.3 手。这样连续回撤日会逐级缩量,反弹创新高再满血恢复。 下面这段 Python 通过 MT5 终端接口实现了上述判断。注意 daily_drop 用 (account_balance - current_balance)/account_balance 算比例,volume 初始 0.3,peak_balance 跟踪前高。 [CODE] def online_trading(symbol, features, model): terminal_path = "C:/Program Files/RoboForex - MetaTrader 5/Arima/terminal64.exe" if not mt5.initialize(path=terminal_path): print("Error: Failed to connect to MetaTrader 5 terminal") return open_trades = 0 e = None attempts = 30000 # Get the current account balance account_info = mt5.account_info() account_balance = account_info.balance # Set the initial volume for opening trades volume = 0.3 # Set the initial peak balance peak_balance = account_balance while True: symbol_info = mt5.symbol_info(symbol) if symbol_info is not None: break else: print("Error: Instrument not found. Attempt {} of {}".format(_ + 1, attempts)) time.sleep(5) while True: price_bid = mt5.symbol_info_tick(symbol).bid price_ask = mt5.symbol_info_tick(symbol).ask signal = model.predict(features) positions_total = mt5.positions_total() # Calculate the daily drop in account balance account_info = mt5.account_info() current_balance = account_info.balance daily_drop = (account_balance - current_balance) / account_balance # Reduce the volume for opening trades by 10% with a step of 0.01 lot for each day of daily drop if daily_drop > 0.02: volume -= 0.01 volume = max(volume, 0.01) elif current_balance > peak_balance: volume = 0.3 peak_balance = current_balance for _ in range(attempts): if positions_total < MAX_OPEN_TRADES and signal[-1] > 0.5: request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": volume, "type": mt5.ORDER_TYPE_BUY, "price": price_ask, "sl": price_ask - 150 * symbol_info.point, [/CODE] 逐行拆关键几行:account_balance 取连接后实盘权益;daily_drop 是相对前次快照的回撤比例;if daily_drop > 0.02 进缩量分支,volume -= 0.01 且地板 0.01;elif current_balance > peak_balance 复位 volume=0.3 并更新峰值。开 MT5 把终端路径改成你自己的,跑起来就能看到回撤日仓位自发变轻。
def online_trading(symbol, features, model): terminal_path = "C:/Program Files/RoboForex - MetaTrader class="num">5/Arima/terminal64.exe" if not mt5.initialize(path=terminal_path): print("Error: Failed to connect to MetaTrader class="num">5 terminal") class="kw">return open_trades = class="num">0 e = None attempts = class="num">30000 # Get the current account balance account_info = mt5.account_info() account_balance = account_info.balance # Set the initial volume for opening trades volume = class="num">0.3 # Set the initial peak balance peak_balance = account_balance while True: symbol_info = mt5.symbol_info(symbol) if symbol_info is not None: class="kw">break else: print("Error: Instrument not found. Attempt {} of {}".format(_ + class="num">1, attempts)) time.sleep(class="num">5) while True: price_bid = mt5.symbol_info_tick(symbol).bid price_ask = mt5.symbol_info_tick(symbol).ask signal = model.predict(features) positions_total = mt5.positions_total() # Calculate the daily drop in account balance account_info = mt5.account_info() current_balance = account_info.balance daily_drop = (account_balance - current_balance) / account_balance # Reduce the volume for opening trades by class="num">10% with a step of class="num">0.01 lot for each day of daily drop if daily_drop > class="num">0.02: volume -= class="num">0.01 volume = max(volume, class="num">0.01) elif current_balance > peak_balance: volume = class="num">0.3 peak_balance = current_balance for _ in range(attempts): if positions_total < MAX_OPEN_TRADES and signal[-class="num">1] > class="num">0.5: request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": volume, "type": mt5.ORDER_TYPE_BUY, "price": price_ask, "sl": price_ask - class="num">150 * symbol_info.point,
「下空单时的请求字段与成交回执」
当持仓数低于 MAX_OPEN_TRADES 且信号值 signal[-1] 小于 0.5 时,脚本会构造一笔 SELL 市价单请求。止损设在 bid 价上方 150 点,止盈设在 bid 价下方 800 点,偏离容差 20 点,魔术码固定为 123456,成交方式用 ORDER_FILLING_FOK——要么按价全成,要么直接不吃单。 request = { "action": mt5.TRADE_ACTION_DEAL, // 立即成交的实盘动作 "symbol": symbol, // 当前交易品种 "volume": volume, // 手数变量 "type": mt5.ORDER_TYPE_SELL, // 空单 "price": price_bid, // 以买价进场 "sl": price_bid + 150 * symbol_info.point, // 空单止损在买价上方150点 "tp": price_bid - 800 * symbol_info.point, // 空单止盈在买价下方800点 "deviation": 20, // 允许20点滑点偏差 "magic": 123456, // 订单魔术标识 "comment": "Test deal", // 订单备注 "type_time": mt5.ORDER_TIME_GTC, // 挂到撤单前一直有效 "type_filling": mt5.ORDER_FILLING_FOK // 填充必须一次性全成 } 发单后靠 mt5.order_send(request) 拿回 result。若 result.retcode 等于 TRADE_RETCODE_DONE,且 signal[-1] 大于 0.5 则打印“Sell position opened”并把 open_trades 加 1,最后返回 result.order 作为订单号。外汇与贵金属杠杆高,FOK 模式在流动性差时可能直接拒单,实盘前建议在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍验证拒单率。
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": volume,
"type": mt5.ORDER_TYPE_SELL,
"price": price_bid,
"sl": price_bid + class="num">150 * symbol_info.point,
"tp": price_bid - class="num">800 * symbol_info.point,
"deviation": class="num">20,
"magic": class="num">123456,
"comment": "Test deal",
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_FOK
}
result = mt5.order_send(request)
if result.retcode == mt5.TRADE_RETCODE_DONE:
if signal[-class="num">1] < class="num">0.5:
print("Buy position opened")
open_trades += class="num">1
elif signal[-class="num">1] > class="num">0.5:
print("Sell position opened")
open_trades += class="num">1
class="kw">return result.order◍ 失败重试里的休眠节奏
上面这段是下单请求未成交时的兜底处理:把返回码和当前重试次数打出来,每次失败后先睡 3 秒,整个外层循环再歇 4000 秒(约 66 分钟)才进入下一轮。 这种长间隔不是偷懒,而是避开流动性极差时段反复撤单被券商限频。若你跑黄金 EA,把 4000 改成 600 可能在数据行情里被连续拒单十几次。 验证方式很简单:开 MT5 策略测试器,用Tick模式跑一段非农夜,观察日志里 Attempt 1/3 与下一次报错之间是否真的隔了 4000 秒左右。外汇与贵金属杠杆高,回测不代表实盘,请先用模拟盘确认。
else: print("Error: Trade request not executed, retcode={}. Attempt {}/{}".format(result.retcode, _ + class="num">1, attempts)) time.sleep(class="num">3) time.sleep(class="num">4000)
用凯利公式把仓位绑在模型置信度上
仓位密度不能拍脑袋定。凯利公式给了一种按胜率和盈亏比反推最优下注比例的数学办法:当模型预测值离 0.5 越远,意味着方向置信度越高,胜率估计值越大,仓位就该相应放大;越靠近 0.5,胜率趋近随机,下注要收缩。 这套逻辑里,胜率用模型输出与 0.5 的距离乘 2 来近似——比如预测值 0.8,胜率记 0.6;预测值 0.55,胜率仅 0.1。最优手数再由凯利表达式 (p - (1-p)/R)/R × 余额 / 现价 算出来,R 是预设盈亏比。 实盘要加两道保险:当日余额回撤超 2% 时,optimal_volume 每次减 0.01 手且地板设为 0.01,避免连亏期继续重仓;当余额刷新历史峰值,按当前权益重算基准仓位并把 peak_balance 上移,让盈利后的扩张有依据。外汇与贵金属杠杆高,回撤保护触发阈值建议先跑历史数据校准,勿直接实盘。 下面这段 Python 对接 MT5 的 online_trading 改法可直接拷去验证,重点看 probability_of_winning 与 optimal_volume 两行,以及 daily_drop>0.02 的分支。
def online_trading(symbol, features, model): terminal_path = "C:/Program Files/RoboForex - MetaTrader class="num">5/Arima/terminal64.exe" if not mt5.initialize(path=terminal_path): print("Error: Failed to connect to MetaTrader class="num">5 terminal") class="kw">return open_trades = class="num">0 e = None attempts = class="num">30000 # Get the current account balance account_info = mt5.account_info() account_balance = account_info.balance # Set the initial volume for opening trades volume = class="num">0.1 # Set the initial peak balance peak_balance = account_balance while True: symbol_info = mt5.symbol_info(symbol) if symbol_info is not None: class="kw">break else: print("Error: Instrument not found. Attempt {} of {}".format(_ + class="num">1, attempts)) time.sleep(class="num">5) while True: price_bid = mt5.symbol_info_tick(symbol).bid price_ask = mt5.symbol_info_tick(symbol).ask signal = model.predict(features) positions_total = mt5.positions_total() # Calculate the daily drop in account balance account_info = mt5.account_info() current_balance = account_info.balance daily_drop = (account_balance - current_balance) / account_balance # Calculate the probability of winning based on the distance between the model&class="macro">#x27;s prediction and class="num">0.5 probability_of_winning = abs(signal[-class="num">1] - class="num">0.5) * class="num">2 # Calculate the optimal volume for opening trades class="kw">using the Kelly criterion optimal_volume = (probability_of_winning - (class="num">1 - probability_of_winning) / risk_reward_ratio) / risk_reward_ratio * account_balance / price_ask # Reduce the volume for opening trades by class="num">10% with a step of class="num">0.01 lot for each day of daily drop if daily_drop > class="num">0.02: optimal_volume -= class="num">0.01 optimal_volume = max(optimal_volume, class="num">0.01) elif current_balance > peak_balance: optimal_volume = (probability_of_winning - (class="num">1 - probability_of_winning) / risk_reward_ratio) / risk_reward_ratio * account_balance / price_ask peak_balance = current_balance # Set the volume for opening trades volume = optimal_volume for _ in range(attempts): if positions_total < MAX_OPEN_TRADES and signal[-class="num">1] > class="num">0.5: request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol,
「多空请求结构与成交回执处理」
这段逻辑把开仓请求按信号方向分成了两套字典。信号值小于 0.5 时走卖单分支,大于 0.5 走买单分支,止损用 150 点、止盈用 800 点,偏离容差设 20 点,magic 编号固定 123456 便于后续在 MT5 里筛选这批测试单。 买单以 price_ask 入场,sl = price_ask - 150 * symbol_info.point,tp = price_ask + 800 * symbol_info.point;卖单镜像处理,sl 在 bid 上加 150 点、tp 在 bid 下减 800 点。两种单都用了 ORDER_TIME_GTC 和 ORDER_FILLING_FOK,意味着挂到取消前有效且必须全部成交否则不撮合。 order_send 之后只认 TRADE_RETCODE_DONE 算真正开仓,此时 open_trades 加 1 并返回 result.order。若信号不在阈值内直接打印无信号并 return None,不会发单。外汇和贵金属杠杆高,FOK 模式在流动性差时可能直接拒单,实盘前建议在策略测试器用 2023 年 EURUSD 数据跑一遍看成交率。
"volume": volume,
"type": mt5.ORDER_TYPE_BUY,
"price": price_ask,
"sl": price_ask - class="num">150 * symbol_info.point,
"tp": price_ask + class="num">800 * symbol_info.point,
"deviation": class="num">20,
"magic": class="num">123456,
"comment": "Test deal",
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_FOK,
}
elif positions_total < MAX_OPEN_TRADES and signal[-class="num">1] < class="num">0.5:
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": volume,
"type": mt5.ORDER_TYPE_SELL,
"price": price_bid,
"sl": price_bid + class="num">150 * symbol_info.point,
"tp": price_bid - class="num">800 * symbol_info.point,
"deviation": class="num">20,
"magic": class="num">123456,
"comment": "Test deal",
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_FOK,
}
else:
print("No signal to open a position")
class="kw">return None
result = mt5.order_send(request)
if result.retcode == mt5.TRADE_RETCODE_DONE:
if signal[-class="num">1] < class="num">0.5:
print("Buy position opened")
open_trades += class="num">1
elif signal[-class="num">1] > class="num">0.5:
print("Sell position opened")
open_trades += class="num">1
class="kw">return result.order◍ 失败重试的退避节奏
上面这段 else 分支处理的是交易请求未被执行的情形。当 result.retcode 不是成功码时,脚本会打印错误码与当前重试次数(_+1 对比总 attempts),并先 sleep(3) 秒做短暂停顿,避免紧接下一次无效请求。 外层循环每次走完若仍未成交,会 sleep(4000) 毫秒(约 66 秒)再进入下一轮尝试。这个退避尺度意味着:在外汇与贵金属这种高波动、高滑点概率的市场里,连续硬怼下单可能触发券商限流或扩大滑点,拉开分钟级间隔更稳。 实际在 MT5 终端里跑这类脚本,建议把 4000 这个值按品种流动性调:主流货币对可压到 2000 内,黄金或交叉盘留 5000 以上更不容易被拒单。
else: print("Error: Trade request not executed, retcode={}. Attempt {}/{}".format(result.retcode, _ + class="num">1, attempts)) time.sleep(class="num">3) time.sleep(class="num">4000)