349 lines
9.3 KiB
Text
349 lines
9.3 KiB
Text
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| TradingView 1:1 Execution EA for MetaTrader 5 |
|
||
|
|
//| Executes the exact signals produced by the TradingView strategy |
|
||
|
|
//| File format: ACTION;SYMBOL;ENTRY;SL;TP1;TP2;TPFINAL;ID |
|
||
|
|
//| Example: BUY;EURUSD;1.08500;1.08300;1.08700;1.08900;1.10100;TV01 |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
#property strict
|
||
|
|
#property version "1.00"
|
||
|
|
|
||
|
|
#include <Trade/Trade.mqh>
|
||
|
|
CTrade trade;
|
||
|
|
|
||
|
|
//============================== INPUTS ==============================
|
||
|
|
|
||
|
|
input string InpSignalFileName = "tv_signal.txt"; // Common/Files/tv_signal.txt
|
||
|
|
input bool InpUseCommonFolder = true;
|
||
|
|
input int InpMagic = 26050601;
|
||
|
|
input double InpLots = 0.10;
|
||
|
|
input bool InpUseRiskPercent = false;
|
||
|
|
input double InpRiskPercent = 1.0;
|
||
|
|
input bool InpUsePartials = true;
|
||
|
|
input double InpTP1Percent = 30.0;
|
||
|
|
input double InpTP2Percent = 30.0;
|
||
|
|
input double InpFinalPercent = 40.0;
|
||
|
|
input int InpSlippagePoints = 20;
|
||
|
|
input bool InpOnePositionAtATime = true;
|
||
|
|
input bool InpCloseOpposite = true;
|
||
|
|
input int InpPollSeconds = 1;
|
||
|
|
|
||
|
|
//============================== GLOBALS ==============================
|
||
|
|
|
||
|
|
string g_last_id = "";
|
||
|
|
datetime g_last_poll = 0;
|
||
|
|
|
||
|
|
//============================== HELPERS ==============================
|
||
|
|
|
||
|
|
string Trim(string s)
|
||
|
|
{
|
||
|
|
StringTrimLeft(s);
|
||
|
|
StringTrimRight(s);
|
||
|
|
return s;
|
||
|
|
}
|
||
|
|
|
||
|
|
int Split(string text, string sep, string &arr[])
|
||
|
|
{
|
||
|
|
return StringSplit(text, StringGetCharacter(sep, 0), arr);
|
||
|
|
}
|
||
|
|
|
||
|
|
double NormalizeLots(double lots)
|
||
|
|
{
|
||
|
|
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
||
|
|
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
|
||
|
|
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
||
|
|
|
||
|
|
lots = MathMax(minLot, MathMin(maxLot, lots));
|
||
|
|
lots = MathFloor(lots / step) * step;
|
||
|
|
|
||
|
|
int digits = 2;
|
||
|
|
if(step == 0.001) digits = 3;
|
||
|
|
if(step == 0.01) digits = 2;
|
||
|
|
if(step == 0.1) digits = 1;
|
||
|
|
|
||
|
|
return NormalizeDouble(lots, digits);
|
||
|
|
}
|
||
|
|
|
||
|
|
double LotsByRisk(double entry, double sl)
|
||
|
|
{
|
||
|
|
if(!InpUseRiskPercent)
|
||
|
|
return NormalizeLots(InpLots);
|
||
|
|
|
||
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
||
|
|
double riskMoney = balance * InpRiskPercent / 100.0;
|
||
|
|
double dist = MathAbs(entry - sl);
|
||
|
|
|
||
|
|
if(dist <= 0.0)
|
||
|
|
return NormalizeLots(InpLots);
|
||
|
|
|
||
|
|
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
|
||
|
|
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
||
|
|
|
||
|
|
if(tickValue <= 0.0 || tickSize <= 0.0)
|
||
|
|
return NormalizeLots(InpLots);
|
||
|
|
|
||
|
|
double lossPerLot = dist / tickSize * tickValue;
|
||
|
|
if(lossPerLot <= 0.0)
|
||
|
|
return NormalizeLots(InpLots);
|
||
|
|
|
||
|
|
return NormalizeLots(riskMoney / lossPerLot);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool IsOurPosition(ulong ticket)
|
||
|
|
{
|
||
|
|
if(!PositionSelectByTicket(ticket))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
if((int)PositionGetInteger(POSITION_MAGIC) != InpMagic)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool HasOurPosition()
|
||
|
|
{
|
||
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
ulong ticket = PositionGetTicket(i);
|
||
|
|
if(ticket == 0) continue;
|
||
|
|
if(IsOurPosition(ticket))
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CloseOppositePositions(bool buySignal)
|
||
|
|
{
|
||
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
ulong ticket = PositionGetTicket(i);
|
||
|
|
if(ticket == 0) continue;
|
||
|
|
if(!IsOurPosition(ticket)) continue;
|
||
|
|
|
||
|
|
long type = PositionGetInteger(POSITION_TYPE);
|
||
|
|
|
||
|
|
if(buySignal && type == POSITION_TYPE_SELL)
|
||
|
|
trade.PositionClose(ticket);
|
||
|
|
|
||
|
|
if(!buySignal && type == POSITION_TYPE_BUY)
|
||
|
|
trade.PositionClose(ticket);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ReadSignalLine(string &line)
|
||
|
|
{
|
||
|
|
int flags = FILE_READ | FILE_TXT | FILE_ANSI;
|
||
|
|
if(InpUseCommonFolder)
|
||
|
|
flags |= FILE_COMMON;
|
||
|
|
|
||
|
|
int handle = FileOpen(InpSignalFileName, flags);
|
||
|
|
if(handle == INVALID_HANDLE)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
line = "";
|
||
|
|
while(!FileIsEnding(handle))
|
||
|
|
{
|
||
|
|
string tmp = FileReadString(handle);
|
||
|
|
if(StringLen(Trim(tmp)) > 0)
|
||
|
|
line = Trim(tmp);
|
||
|
|
}
|
||
|
|
|
||
|
|
FileClose(handle);
|
||
|
|
return StringLen(line) > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ExecuteSingleOrder(bool isBuy, double lots, double sl, double tp, string comment)
|
||
|
|
{
|
||
|
|
trade.SetExpertMagicNumber(InpMagic);
|
||
|
|
trade.SetDeviationInPoints(InpSlippagePoints);
|
||
|
|
|
||
|
|
bool ok = false;
|
||
|
|
|
||
|
|
if(isBuy)
|
||
|
|
ok = trade.Buy(lots, _Symbol, 0.0, sl, tp, comment);
|
||
|
|
else
|
||
|
|
ok = trade.Sell(lots, _Symbol, 0.0, sl, tp, comment);
|
||
|
|
|
||
|
|
if(!ok)
|
||
|
|
{
|
||
|
|
Print("Order failed: ", comment,
|
||
|
|
" retcode=", trade.ResultRetcode(),
|
||
|
|
" desc=", trade.ResultRetcodeDescription());
|
||
|
|
}
|
||
|
|
|
||
|
|
return ok;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ExecuteSignal(string line)
|
||
|
|
{
|
||
|
|
string p[];
|
||
|
|
int n = Split(line, ";", p);
|
||
|
|
|
||
|
|
if(n < 8)
|
||
|
|
{
|
||
|
|
Print("Invalid signal format. Expected 8 fields: ", line);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
string action = StringUpper(Trim(p[0]));
|
||
|
|
string symbol = Trim(p[1]);
|
||
|
|
|
||
|
|
if(symbol != _Symbol)
|
||
|
|
{
|
||
|
|
Print("Signal ignored. Signal symbol=", symbol, " chart symbol=", _Symbol);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
double entry = StringToDouble(Trim(p[2]));
|
||
|
|
double sl = NormalizeDouble(StringToDouble(Trim(p[3])), _Digits);
|
||
|
|
double tp1 = NormalizeDouble(StringToDouble(Trim(p[4])), _Digits);
|
||
|
|
double tp2 = NormalizeDouble(StringToDouble(Trim(p[5])), _Digits);
|
||
|
|
double tpFinal = NormalizeDouble(StringToDouble(Trim(p[6])), _Digits);
|
||
|
|
string id = Trim(p[7]);
|
||
|
|
|
||
|
|
if(id == "" || id == g_last_id)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
bool isBuy = action == "BUY" || action == "LONG" || action == "BULL";
|
||
|
|
bool isSell = action == "SELL" || action == "SHORT" || action == "BEAR";
|
||
|
|
|
||
|
|
if(!isBuy && !isSell)
|
||
|
|
{
|
||
|
|
Print("Invalid action: ", action);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if(InpCloseOpposite)
|
||
|
|
CloseOppositePositions(isBuy);
|
||
|
|
|
||
|
|
if(InpOnePositionAtATime && HasOurPosition())
|
||
|
|
{
|
||
|
|
Print("Signal ignored: existing position and InpOnePositionAtATime=true");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
double lotsTotal = LotsByRisk(entry, sl);
|
||
|
|
if(lotsTotal <= 0.0)
|
||
|
|
{
|
||
|
|
Print("Invalid lots");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ok = true;
|
||
|
|
|
||
|
|
if(!InpUsePartials)
|
||
|
|
{
|
||
|
|
ok = ExecuteSingleOrder(isBuy, lotsTotal, sl, tpFinal, "TV 1:1 " + id);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
double lot1 = NormalizeLots(lotsTotal * InpTP1Percent / 100.0);
|
||
|
|
double lot2 = NormalizeLots(lotsTotal * InpTP2Percent / 100.0);
|
||
|
|
double lot3 = NormalizeLots(lotsTotal * InpFinalPercent / 100.0);
|
||
|
|
|
||
|
|
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
||
|
|
|
||
|
|
if(lot1 < minLot) lot1 = 0.0;
|
||
|
|
if(lot2 < minLot) lot2 = 0.0;
|
||
|
|
if(lot3 < minLot) lot3 = 0.0;
|
||
|
|
|
||
|
|
if(lot1 + lot2 + lot3 <= 0.0)
|
||
|
|
lot3 = lotsTotal;
|
||
|
|
|
||
|
|
if(lot1 > 0.0)
|
||
|
|
ok = ExecuteSingleOrder(isBuy, lot1, sl, tp1, "TV 1:1 TP1 " + id) && ok;
|
||
|
|
|
||
|
|
if(lot2 > 0.0)
|
||
|
|
ok = ExecuteSingleOrder(isBuy, lot2, sl, tp2, "TV 1:1 TP2 " + id) && ok;
|
||
|
|
|
||
|
|
if(lot3 > 0.0)
|
||
|
|
ok = ExecuteSingleOrder(isBuy, lot3, sl, tpFinal, "TV 1:1 FINAL " + id) && ok;
|
||
|
|
}
|
||
|
|
|
||
|
|
if(ok)
|
||
|
|
{
|
||
|
|
g_last_id = id;
|
||
|
|
Print("Executed TradingView signal: ", line);
|
||
|
|
}
|
||
|
|
|
||
|
|
return ok;
|
||
|
|
}
|
||
|
|
|
||
|
|
//============================== MT5 EVENTS ============================
|
||
|
|
|
||
|
|
int OnInit()
|
||
|
|
{
|
||
|
|
trade.SetExpertMagicNumber(InpMagic);
|
||
|
|
EventSetTimer(InpPollSeconds);
|
||
|
|
|
||
|
|
Print("TradingView 1:1 Execution EA started.");
|
||
|
|
Print("Signal file: Common/Files/", InpSignalFileName);
|
||
|
|
Print("Format: ACTION;SYMBOL;ENTRY;SL;TP1;TP2;TPFINAL;ID");
|
||
|
|
|
||
|
|
return INIT_SUCCEEDED;
|
||
|
|
}
|
||
|
|
|
||
|
|
void OnDeinit(const int reason)
|
||
|
|
{
|
||
|
|
EventKillTimer();
|
||
|
|
Print("TradingView 1:1 Execution EA stopped.");
|
||
|
|
}
|
||
|
|
|
||
|
|
void OnTick()
|
||
|
|
{
|
||
|
|
// Trading is handled in OnTimer so it can react even without a new tick.
|
||
|
|
}
|
||
|
|
|
||
|
|
void OnTimer()
|
||
|
|
{
|
||
|
|
string line;
|
||
|
|
if(ReadSignalLine(line))
|
||
|
|
ExecuteSignal(line);
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
|
||
|
|
|
||
|
|
//====================================================================
|
||
|
|
// TradingView Alert-Format für deine Pine Strategy
|
||
|
|
//====================================================================
|
||
|
|
//
|
||
|
|
// Damit der MT5-EA 1:1 dieselben Signale handelt, muss TradingView
|
||
|
|
// bei jedem echten Strategy-Signal eine Zeile in folgendem Format
|
||
|
|
// an deinen Bridge-Dienst senden:
|
||
|
|
//
|
||
|
|
// BUY;{{ticker}};ENTRY;SL;TP1;TP2;TPFINAL;UNIQUE_ID
|
||
|
|
// SELL;{{ticker}};ENTRY;SL;TP1;TP2;TPFINAL;UNIQUE_ID
|
||
|
|
//
|
||
|
|
// In deinem Pine Script sind die Werte bereits vorhanden:
|
||
|
|
//
|
||
|
|
// Bear confirmed:
|
||
|
|
// bear_entry, bear_sl, short_tp1, short_tp2, bear_tp
|
||
|
|
//
|
||
|
|
// Bull confirmed:
|
||
|
|
// bull_entry, bull_sl, long_tp1, long_tp2, bull_tp
|
||
|
|
//
|
||
|
|
// Final bear:
|
||
|
|
// bear_entry, bear_sl, short_tp1, short_tp2, bear_tp
|
||
|
|
//
|
||
|
|
// Final bull:
|
||
|
|
// bull_entry, bull_sl, long_tp1, long_tp2, bull_tp
|
||
|
|
//
|
||
|
|
// Beispiel Alert-Message in Pine:
|
||
|
|
//
|
||
|
|
// alert("SELL;" + syminfo.ticker + ";" +
|
||
|
|
// str.tostring(bear_entry, format.mintick) + ";" +
|
||
|
|
// str.tostring(bear_sl, format.mintick) + ";" +
|
||
|
|
// str.tostring(short_tp1, format.mintick) + ";" +
|
||
|
|
// str.tostring(short_tp2, format.mintick) + ";" +
|
||
|
|
// str.tostring(bear_tp, format.mintick) + ";" +
|
||
|
|
// "FINAL_BEAR_" + str.tostring(time),
|
||
|
|
// alert.freq_once_per_bar)
|
||
|
|
//
|
||
|
|
// Der Bridge-Dienst schreibt diese Message als letzte Zeile in:
|
||
|
|
// MetaTrader 5 / Common / Files / tv_signal.txt
|
||
|
|
//
|
||
|
|
// Dadurch bestimmt TradingView exakt Entry, SL und TP.
|
||
|
|
// MT5 führt nur aus.
|
||
|
|
//====================================================================
|