293 lines
8.1 KiB
Plaintext
293 lines
8.1 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| SwapMonitor.mq5 |
|
|
//| Copyright (c) 2024, Marketeer |
|
|
//| https://www.mql5.com/en/users/marketeer |
|
|
//+------------------------------------------------------------------+
|
|
|
|
#property service
|
|
#property copyright "Copyright (c) 2024 Marketeer"
|
|
#property link "https://www.mql5.com/en/users/marketeer"
|
|
#property version "1.0"
|
|
#property script_show_inputs
|
|
|
|
#include <MQL5Book/ArrayUtils.mqh>
|
|
#include <MQL5Book/Defines.mqh>
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| I N P U T S |
|
|
//+------------------------------------------------------------------+
|
|
|
|
input string SymbolList = ""; // SymbolList (comma,separated)
|
|
input string Folder = "Swaps";
|
|
input uint Beat = 10; // Beat (seconds)
|
|
input bool AutoSelect = false;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Global definitions |
|
|
//+------------------------------------------------------------------+
|
|
|
|
const string Program = EnumToString((ENUM_PROGRAM_TYPE)MQLInfoInteger(MQL_PROGRAM_TYPE)) + " " + MQLInfoString(MQL_PROGRAM_NAME);
|
|
|
|
string SymbolArray[];
|
|
|
|
struct Swaps // could be null-ed on creation by compiler if static and global (not the case)
|
|
{
|
|
datetime dt;
|
|
double L;
|
|
double S;
|
|
int N;
|
|
|
|
Swaps()
|
|
{
|
|
ZeroMemory(this);
|
|
}
|
|
|
|
Swaps(const datetime t, const double l, const double s, const int n = 0): dt(t), L(l), S(s), N(n) { }
|
|
|
|
string csv() const
|
|
{
|
|
return StringFormat("%s,%g,%g", TimeToString(dt, TIME_DATE | TIME_SECONDS), L, S);
|
|
}
|
|
};
|
|
|
|
struct Position
|
|
{
|
|
ulong ticket;
|
|
double swap;
|
|
};
|
|
|
|
Swaps cache[];
|
|
Position positions[];
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Service program start function |
|
|
//+------------------------------------------------------------------+
|
|
|
|
void OnStart()
|
|
{
|
|
if(StringSplit(SymbolList, ',', SymbolArray) < 1)
|
|
{
|
|
Print("No symbols given");
|
|
if(AutoSelect)
|
|
{
|
|
for(int i = 0; i < SymbolsTotal(true); i++)
|
|
{
|
|
const string symbol = SymbolName(i, true);
|
|
if(!SymbolInfoInteger(symbol, SYMBOL_CUSTOM)
|
|
&& SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE) != SYMBOL_TRADE_MODE_DISABLED)
|
|
{
|
|
PUSH(SymbolArray, symbol);
|
|
}
|
|
}
|
|
Print("MarketWatch symbols autoselected:");
|
|
ArrayPrint(SymbolArray);
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < ArraySize(SymbolArray); i++)
|
|
{
|
|
if(!SymbolInfoInteger(SymbolArray[i], SYMBOL_EXIST))
|
|
{
|
|
Print("Unknown symbol: ", SymbolArray[i]);
|
|
SymbolArray[i] = "";
|
|
}
|
|
else if(!SymbolInfoInteger(SymbolArray[i], SYMBOL_VISIBLE))
|
|
{
|
|
if(AutoSelect && SymbolSelect(SymbolArray[i], true))
|
|
{
|
|
// ok
|
|
}
|
|
else
|
|
{
|
|
PrintFormat("Can't select symbol: %s [%d]", SymbolArray[i], _LastError);
|
|
SymbolArray[i] = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
ArrayPurge(SymbolArray, "");
|
|
ArrayResize(cache, ArraySize(SymbolArray));
|
|
|
|
Print(Program + " started");
|
|
bool active = true;
|
|
|
|
for(; !IsStopped() ;)
|
|
{
|
|
// wait for connected and logged in state
|
|
if(!(TerminalInfoInteger(TERMINAL_CONNECTED) && AccountInfoInteger(ACCOUNT_LOGIN)))
|
|
{
|
|
if(active)
|
|
{
|
|
Print(Program + " is waiting for connection");
|
|
active = false;
|
|
}
|
|
|
|
Sleep(1000);
|
|
continue;
|
|
}
|
|
|
|
if(!active)
|
|
{
|
|
Print(Program + " connected");
|
|
active = true;
|
|
}
|
|
|
|
// check swaps in symbols properties and save changes (if any)
|
|
const string filename = GetTimeStamp();
|
|
for(int i = 0; i < ArraySize(SymbolArray); i++)
|
|
{
|
|
if(StringLen(SymbolArray[i]) && !SaveSymbolSwap(SymbolArray[i], filename, cache[i]))
|
|
{
|
|
SymbolArray[i] = "";
|
|
}
|
|
}
|
|
|
|
// check if positions still exist
|
|
for(int i = 0; i < ArraySize(positions); i++)
|
|
{
|
|
if(!PositionSelectByTicket(positions[i].ticket))
|
|
{
|
|
Alert(StringFormat("Position %llu closed with swap %g", positions[i].ticket, positions[i].swap));
|
|
positions[i].ticket = 0;
|
|
}
|
|
}
|
|
|
|
ArrayPurger<Position> purger(positions, IsClosedPosition);
|
|
|
|
// compare positions swaps and alert changes (if any)
|
|
const int n = PositionsTotal();
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
const ulong ticket = PositionGetTicket(i);
|
|
if(ticket)
|
|
{
|
|
FindPositionSwap(ticket);
|
|
}
|
|
}
|
|
|
|
Sleep(Beat * 1000);
|
|
}
|
|
Print(Program + " stopped");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Auxiliary functions |
|
|
//+------------------------------------------------------------------+
|
|
|
|
string GetTimeStamp()
|
|
{
|
|
MqlDateTime dt;
|
|
TimeToStruct(TimeCurrent(), dt);
|
|
return StringFormat("%04d%02d", dt.year, dt.mon) + ".csv";
|
|
}
|
|
|
|
template<typename T>
|
|
bool Contains(const T &a[], const T v)
|
|
{
|
|
for(int i = 0; i < ArraySize(a); i++)
|
|
{
|
|
if(a[i] == v) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
string Escape(const string s)
|
|
{
|
|
uchar a[];
|
|
uchar forbidden[] = {'<', '>', '/', '\\', '"', ':', '|', '*', '?'};
|
|
int n = StringToCharArray(s, a);
|
|
for(int i = 0; i < n - 1; i++)
|
|
{
|
|
if(a[i] < 32 || Contains(forbidden, a[i]))
|
|
{
|
|
a[i] = '_';
|
|
}
|
|
}
|
|
return CharArrayToString(a);
|
|
}
|
|
|
|
bool IsClosedPosition(const Position &p)
|
|
{
|
|
return !p.ticket;
|
|
}
|
|
|
|
void FindPositionSwap(const ulong ticket)
|
|
{
|
|
const double swap = PositionGetDouble(POSITION_SWAP);
|
|
const double volume = PositionGetDouble(POSITION_VOLUME);
|
|
const string symbol = PositionGetString(POSITION_SYMBOL);
|
|
const int digits = (int)MathLog10(1.0 / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP));
|
|
const string type = PositionGetInteger(POSITION_TYPE) ? "Sell" : "Buy";
|
|
|
|
for(int i = 0; i < ArraySize(positions); i++)
|
|
{
|
|
if(positions[i].ticket == ticket)
|
|
{
|
|
if(positions[i].swap != swap)
|
|
{
|
|
Alert(StringFormat("Position %llu (%s %.*f %s) swap: %g -> %g",
|
|
ticket, type, digits, volume, symbol, positions[i].swap, swap));
|
|
positions[i].swap = swap;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
Position p = {ticket, swap};
|
|
PUSH(positions, p);
|
|
Alert(StringFormat("New position %llu (%s %.*f %s) swap: %g",
|
|
ticket, type, digits, volume, symbol, swap));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Persistent storage (CSV-files) |
|
|
//+------------------------------------------------------------------+
|
|
|
|
bool SaveSymbolSwap(const string &s, const string &f, Swaps &last)
|
|
{
|
|
const Swaps now
|
|
(
|
|
TimeCurrent(),
|
|
SymbolInfoDouble(s, SYMBOL_SWAP_LONG),
|
|
SymbolInfoDouble(s, SYMBOL_SWAP_SHORT),
|
|
last.N
|
|
);
|
|
|
|
if(now.L == last.L && now.S == last.S) return true; // up to date
|
|
|
|
const string name = Folder + "/" + Escape(s) + "/" + f;
|
|
int h = FileOpen(name, FILE_ANSI | FILE_TXT | FILE_READ | FILE_WRITE, ",", CP_UTF8);
|
|
if(h == INVALID_HANDLE)
|
|
{
|
|
PrintFormat("Can't open file '%s' [%d]", name, _LastError);
|
|
return false;
|
|
}
|
|
if(!FileSize(h))
|
|
{
|
|
FileWrite(h, "T,L,S");
|
|
}
|
|
else
|
|
{
|
|
FileSeek(h, 0, SEEK_END);
|
|
}
|
|
|
|
FileWrite(h, now.csv());
|
|
|
|
FileClose(h);
|
|
|
|
PrintFormat("%s swap changed %d-th time", s, last.N + 1);
|
|
if(last.dt)
|
|
{
|
|
PrintFormat("- Old: %s L=%g S=%g", TimeToString(last.dt, TIME_DATE | TIME_SECONDS), last.L, last.S);
|
|
}
|
|
PrintFormat("+ New: %s L=%g S=%g", TimeToString(now.dt, TIME_DATE | TIME_SECONDS), now.L, now.S);
|
|
Alert(StringFormat("%s swaps: L=%g S=%g", s, now.L, now.S));
|
|
|
|
last = now;
|
|
last.N++;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|