开仓细节与策略逻辑实现
一、开仓核心逻辑总览
OnTick()
方法是EA的“心脏”,负责实时行情处理与交易执行。本章节以RSI反转策略为例,解析从信号判断到订单下达的完整开仓流程,重点包括新K线过滤、持仓检查、条件判断、订单参数计算四大模块。
二、新K线过滤:避免实时波动干扰
isNewBar()
函数:- 作用:判断是否产生新K线(基于收盘价形成),避免在K线未闭合时执行逻辑(防止实时价格波动误导信号)。
- 实现原理:通过对比当前K线与前一根K线K线总数,确认新K线已形成。
cpp
bool isNewBar(string symbol, ENUM_TIMEFRAMES tf, int &bTotal) {
int bars = iBars(_Symbol, tf);
if (bars == bTotal)
return false;
bTotal = bars;
return true;
}
三、持仓统计:避免重复开仓
1. 持仓计数函数
cpp
// &cntBuy &cntSell 是引用参数, 他们的作用是将函数内部的计算结果传递回函数外部
bool countOpenPositions(string symbol, int &cntBuy, int &cntSell) {
cntBuy = 0;
cntSell = 0;
int total = PositionsTotal();
// 倒序循环可以避免因关闭订单循环索引变化导致跳过元素的问题
for (int i = total - 1; i >= 0; i--) {
// 获取第i个仓位的订单号,并将其赋值给positionTicket,每个订单都有唯一的订单号
ulong ticket = PositionGetTicket(i);
if (ticket <= 0) {
Print("获取ticket失败");
return false;
}
if (!PositionSelectByTicket(ticket)) {
Print("选中ticket失败");
return false;
}
long magic;
if (!PositionGetInteger(POSITION_MAGIC, magic)) {
Print("获取魔术号失败");
return false;
}
if (magic == MagicNum) {
long type;
if (!PositionGetInteger(POSITION_TYPE, type)) {
Print("获取持仓类型失败");
return false;
};
if (type == POSITION_TYPE_BUY) {
cntBuy++;
} else if (type == POSITION_TYPE_SELL) {
cntSell++;
}
}
}
return true;
}
- 核心逻辑:
- 遍历所有持仓,通过
MagicNumber
筛选属于当前EA的订单。 - 区分多单(
POSITION_TYPE_BUY
)和空单(POSITION_TYPE_SELL
),分别计数。 - 使用引用传递(
int &countBuy
)将结果返回给调用方。
- 遍历所有持仓,通过
2. 持仓检查
cpp
int countBuy = 0, countSell = 0;
CountOpenPositions(MagicNumber, countBuy, countSell);
if(countBuy > 0 || countSell > 0) return; // 有持仓则跳过开仓
- 作用:确保EA同一时间仅持有单方向仓位(避免多空对冲或重复开仓)。
四、开仓条件:
以多单开仓条件为例(空单对称):
cpp
// RSILevel 表示上轨 (100 - RSILevel) 表示下轨
bool bullSignal = cntBuy == 0 && RsiBuffer[2] >= (100 - RSILevel) &&
RsiBuffer[1] < (100 - RSILevel);
if (UseMAFilter) {
bullSignal = bullSignal && ask > MABuffer[1];
}
1. RSI超卖逻辑
- 核心条件:
rsiBuffer[2] > 下轨
(前前K线RSI高于下轨)rsiBuffer[1] < 下轨
(前K线RSI低于下轨)- 逻辑:通过两根K线的RSI穿越下轨,判断超卖反转信号(如RSI_Level=70时,下轨=30,形成“RSI穿越下轨”超卖的信号)。
- 注意: 数组使用下标
[1],[2]
, 而不用[0]
, 因为[0]
表示当前尚未闭合的K线
2. MA趋势过滤
- 条件:
ask > MABuffer[1];
(现价高于MA,趋势看涨) - 作用:仅在价格位于均线上方时开多单,过滤逆趋势信号,提升信号质量。
五、止损止盈与仓位计算
1. 价格计算(三元表达式简化逻辑)
cpp
double sl = StopLoss == 0 ? 0 : ask - StopLoss * _Point; // 多单止损价
double tp = TakeProfit == 0 ? 0 : ask + TakeProfit * _Point; // 多单止盈价
- 三元表达式:等价于
if-else
,简化代码结构,提升可读性。
2. 价格规范化
cpp
NormalizePrice(_Symbol, sl); // 确保价格符合品种最小报价单位
NormalizePrice(_Symbol, tp);
NormalizePrice()
函数:- 作用: 将品种的价格规范化为最小变动单位的整数倍。
cpp
bool NormalizePrice(string symbol, double &price) {
// 获取 交易品种symbol最小交易单位大小tickSize
double tickSize = 0;
if (!SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE, tickSize)) {
Print("SYMBOL_TRADE_TICK_SIZE 获取异常");
return false;
}
// 获取交易品种symbol的小数点位数digits
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
// 将价格price除以tickSize取整,再乘以tickSize的到规范化后的价格
// 使用NormalizeDouble函数将价格舍入到指定的小鼠点位数digits
price = NormalizeDouble(MathRound(price / tickSize) * tickSize, digits);
return true;
}
3. 仓位计算
cpp
double slp = SlParam;
if (SlType == 4) {
slp = calculateEachEaBalance(MagicNum, initBalance); // 子账户百分之一
}
double lotSize = calcLots(_Symbol, bid, sl, SlType, slp);
- 具体实现:参考4种资金管理方式。
六、反向信号平仓
CloseAllPositions()
函数:- 作用:根据信号方向,平掉反向持仓(如开多单前平空单),确保单一方向持仓。
- 实现:遍历持仓,按类型(多/空)平仓,避免多空同时持仓增加风险。
cpp
// 平仓
bool closePositions(long magicNum, int posType) {
int total = PositionsTotal();
for (int i = total - 1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if (ticket < 0) {
Print("获取ticket失败");
return false;
}
if (!PositionSelectByTicket(ticket)) {
Print("选中ticket失败");
return false;
}
long magic;
if (PositionGetInteger(POSITION_MAGIC, magic)) {
Print("获取magic失败");
return false;
}
if (magicNum == magic) {
long type;
if (!PositionGetInteger(POSITION_TYPE, type)) {
Print("获取持仓类型失败");
return false;
}
//如果要平的仓位是多单,但是当前选择的仓位类型是空单,就跳出本次循环,继续下一个仓位的处理
if (posType == POSITION_TYPE_BUY && type == POSITION_TYPE_SELL)
continue;
//如果要平的仓位是空单,但是当前选择的仓位类型是多单,就跳出本次循环,继续下一个仓位的处理
if (posType == POSITION_TYPE_SELL && type == POSITION_TYPE_BUY)
continue;
trade.PositionClose(ticket);
if (trade.ResultRetcode() != TRADE_RETCODE_DONE) {
Print("平仓失败", ticket, " 异常:", (string)trade.ResultRetcode(), " :",
trade.ResultRetcodeDescription());
}
}
}
return true;
}
七、自定义NormalizePrice函数和 直接使用NormalizeDouble()的区别
- 自定义
NormalizePrice()
函数:
例如 tickSize = 0.01,digits = 2 , 将100.123规范成100.12 - 直接使用
NormalizeDouble(price,_Digits)
函数:
如果 tickSize 不是0.01(例如 0.05),输出的 100.12 可能不是有效的价格,因为它不是0.05的整数倍。 - 跨品种兼容性: 不同交易品种的 tickSize 和 digits 可能不同(例如,外汇是 0.00001,股票可能是 0.01),NormalizePrice 能动态适配。