forked from animatedread/Warrior_EA
173 lines
7.8 KiB
MQL5
173 lines
7.8 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| TradeBook.mqh |
| |||
//| AnimateDread |
| |||
//| |
| |||
//| THE JOURNAL OF REAL TRADES, one line each, written when they |
| |||
//| CLOSE: what the trader saw (the whole SMarketContext), what they |
| |||
//| decided (size, and why), and what happened (R, the worst and best |
| |||
//| the trade was ever worth, how long it took, how it ended). |
| |||
//| |
| |||
//| This is not Database\WarriorJournal.mqh. That one journals every |
| |||
//| FILTER FIRING as a virtual trade against invented barriers, to |
| |||
//| rank modules. This one records the trades the account actually |
| |||
//| took, at the barriers it actually had, with the costs it actually |
| |||
//| paid - the only kind of evidence the Mind acts on. |
| |||
//| |
| |||
//| R = net result / the money the stop put at risk when the trade |
| |||
//| opened. It is computed from the broker's own OrderCalcProfit, so |
| |||
//| it needs no contract-size arithmetic here and includes swap and |
| |||
//| commission. MAE/MFE are read from the bars the trade lived through, |
| |||
//| not from ticks, so they are the same under any tester model. |
| |||
//| |
| |||
//| FILE: Common\Files\Warrior_EA\Mind\book_<SYMBOL>_<TF>.csv |
| |||
//| * tester -> truncated at Init. A backtest's book is that |
| |||
//| backtest's, never last week's. |
| |||
//| * live -> appended, and read back at Init so the reviewer |
| |||
//| resumes where the account left off. |
| |||
//+------------------------------------------------------------------+
| |||
#ifndef WARRIOR_TRADEBOOK_MQH
| |||
#define WARRIOR_TRADEBOOK_MQH
| |||
| |||
#include "MarketContext.mqh"
| |||
| |||
//--- A trade from the moment it opens to the moment it is filed.
| |||
struct STradeRecord
| |||
{
| |||
ulong position; // position id
| |||
datetime openTime, closeTime;
| |||
int side; // +1 long, -1 short
| |||
double entry, exit, sl, lots;
| |||
double riskMoney; // what the stop put at risk when it opened
| |||
double net; // profit + swap + commission
| |||
double r; // net / riskMoney
| |||
double maeR, mfeR; // worst / best excursion, in R (mae <= 0 <= mfe)
| |||
int bars; // bars the trade lived through
| |||
string reason; // stop / stopout / expert
| |||
double scale; // risk multiple applied to the base (1.0 = untouched)
| |||
double reviewP; // the reviewer's P(win), or CTX_NA
| |||
int reviewN; // trades behind it
| |||
SMarketContext ctx;
| |||
};
| |||
| |||
class CTradeBook
| |||
{
| |||
protected:
| |||
string m_path;
| |||
bool m_live;
| |||
int m_written;
| |||
| |||
static string BaseHeader(void)
| |||
{
| |||
return "position,open_time,close_time,side,entry,exit,sl,lots,risk_money,net,r,mae_r,mfe_r,"
| |||
"bars,reason,scale,review_p,review_n,";
| |||
}
| |||
| |||
public:
| |||
CTradeBook(void) : m_path(""), m_live(false), m_written(0) {}
| |||
//--- Opens (tester: truncates; live: keeps) the file for this symbol and timeframe.
| |||
bool Init(const string symbol, const ENUM_TIMEFRAMES tf, const bool tester);
| |||
string Path(void) const { return m_path; }
| |||
int Written(void) const { return m_written; }
| |||
void Append(const STradeRecord &t);
| |||
//--- Live only: replay the file, handing each row's context and R to the caller.
| |||
//--- Rows are parsed by the width of the header this build writes; a file from a build with
| |||
//--- different fields is refused rather than mis-read.
| |||
int Load(SMarketContext &ctxs[], double &rs[]) const;
| |||
};
| |||
//+------------------------------------------------------------------+
| |||
bool CTradeBook::Init(const string symbol, const ENUM_TIMEFRAMES tf, const bool tester)
| |||
{
| |||
FolderCreate("Warrior_EA", FILE_COMMON);
| |||
FolderCreate("Warrior_EA\\Mind", FILE_COMMON);
| |||
m_path = "Warrior_EA\\Mind\\book_" + symbol + "_" + IntegerToString((int)tf) + ".csv";
| |||
m_live = !tester;
| |||
const string header = BaseHeader() + CtxHeader() + ",story";
| |||
if(tester || !FileIsExist(m_path, FILE_COMMON))
| |||
{
| |||
const int h = FileOpen(m_path, FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_COMMON);
| |||
if(h == INVALID_HANDLE)
| |||
{
| |||
PrintFormat("CTradeBook: cannot write %s (error %d) - no book will be kept.", m_path, GetLastError());
| |||
m_path = "";
| |||
return false;
| |||
}
| |||
FileWriteString(h, header + "\r\n");
| |||
FileClose(h);
| |||
return true;
| |||
}
| |||
//--- Live restart: the existing file must be one this build can read.
| |||
const int h = FileOpen(m_path, FILE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON);
| |||
if(h == INVALID_HANDLE)
| |||
return false;
| |||
const string first = FileReadString(h);
| |||
FileClose(h);
| |||
if(first != header)
| |||
{
| |||
PrintFormat("CTradeBook: %s was written by a different build (columns differ) - starting a"
| |||
" new book beside it.", m_path);
| |||
m_path = "Warrior_EA\\Mind\\book_" + symbol + "_" + IntegerToString((int)tf) + "_" +
| |||
TimeToString(TimeLocal(), TIME_DATE) + ".csv";
| |||
const int w = FileOpen(m_path, FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_COMMON);
| |||
if(w == INVALID_HANDLE)
| |||
{
| |||
m_path = "";
| |||
return false;
| |||
}
| |||
FileWriteString(w, header + "\r\n");
| |||
FileClose(w);
| |||
}
| |||
return true;
| |||
}
| |||
//+------------------------------------------------------------------+
| |||
void CTradeBook::Append(const STradeRecord &t)
| |||
{
| |||
if(m_path == "")
| |||
return;
| |||
const int h = FileOpen(m_path, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_COMMON);
| |||
if(h == INVALID_HANDLE)
| |||
return;
| |||
FileSeek(h, 0, SEEK_END);
| |||
string story = t.ctx.story;
| |||
StringReplace(story, ",", ";");
| |||
StringReplace(story, "\n", " ");
| |||
const string row = StringFormat("%I64u,%s,%s,%d,%.5f,%.5f,%.5f,%.2f,%.2f,%.2f,%.4f,%.4f,%.4f,%d,%s,%.2f,%s,%d,",
| |||
t.position, TimeToString(t.openTime, TIME_DATE | TIME_MINUTES),
| |||
TimeToString(t.closeTime, TIME_DATE | TIME_MINUTES), t.side,
| |||
t.entry, t.exit, t.sl, t.lots, t.riskMoney, t.net, t.r, t.maeR,
| |||
t.mfeR, t.bars, t.reason, t.scale,
| |||
(t.reviewP == CTX_NA ? "" : DoubleToString(t.reviewP, 4)), t.reviewN)
| |||
+ CtxRow(t.ctx) + "," + story;
| |||
FileWriteString(h, row + "\r\n");
| |||
FileClose(h);
| |||
m_written++;
| |||
}
| |||
//+------------------------------------------------------------------+
| |||
int CTradeBook::Load(SMarketContext &ctxs[], double &rs[]) const
| |||
{
| |||
if(m_path == "" || !m_live)
| |||
return 0;
| |||
const int h = FileOpen(m_path, FILE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON);
| |||
if(h == INVALID_HANDLE)
| |||
return 0;
| |||
FileReadString(h); // header
| |||
const int base = 18; // columns before the context
| |||
int n = 0;
| |||
while(!FileIsEnding(h))
| |||
{
| |||
const string line = FileReadString(h);
| |||
string f[];
| |||
const int cols = StringSplit(line, ',', f);
| |||
if(cols < base + CTX_N)
| |||
continue;
| |||
ArrayResize(ctxs, n + 1, 64);
| |||
ArrayResize(rs, n + 1, 64);
| |||
ctxs[n].Reset(0);
| |||
for(int i = 0; i < CTX_N; i++)
| |||
ctxs[n].v[i] = StringToDouble(f[base + i]);
| |||
rs[n] = StringToDouble(f[10]); // the `r` column
| |||
n++;
| |||
}
| |||
FileClose(h);
| |||
return n;
| |||
}
| |||
#endif // WARRIOR_TRADEBOOK_MQH
|