| //+------------------------------------------------------------------+
|
| //| 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
|