preview
State Persistence in MQL5 (Part 1): A Crash-Safe State Store That Survives a Restart

State Persistence in MQL5 (Part 1): A Crash-Safe State Store That Survives a Restart

MetaTrader 5 — Trading systems |
146 0
Martin Alejandro Bamonte
Martin Alejandro Bamonte

Contents

  1. An advisor that forgets
  2. The save that corrupts itself
  3. Knowing when the file is wrong
  4. Beyond single numbers
  5. The class
  6. Seeing it survive
  7. Files and how to run it
  8. Limitations
  9. What Part 2 adds
  10. Conclusion


An advisor that forgets

Every Expert Advisor carries state that lives only in memory. The current martingale step, the grid level it has reached, how many trades a daily counter has seen, the weights of a small model it trained on the fly. Close the terminal, let it update overnight, or let it crash once, and any of that wipes the in-memory state. The next tick starts from zero, and the advisor makes a decision that ignores everything it was doing a minute earlier.

Consider a recovery advisor that scales into a basket. It keeps in memory that it is three levels deep and requires a specific set of exits. The terminal restarts overnight for an update. In the morning the advisor starts with an empty memory, sees the same open positions it opened yesterday, and has no idea they are its own. It either double-counts them and adds a fourth level it never intended, or it ignores them and manages nothing. The account did not change while the terminal was down; only the advisor's in-memory view did. That gap is where losses can occur. The runtime state of an advisor is part of the strategy, and losing it silently is a costly bug.

MQL5 already gives us three well-known ways to keep state on disk. The first writes a fixed-size structure to a binary file, as in the article Keeping Memory Across Restarts: EA State Persistence Using Binary Files in MQL5. The second is a key-value store over a flat file, covered recently in the article Persistent Key-Value Store in MQL5: Using Flat Files as a Lightweight Database for EA State. The third is a database, with the SQLite engine the platform ships natively, which the series Engineering a Self-Healing Expert Advisor in MQL5 uses to persist trade state. SQLite, used with its transactions, already solves the write-safety problem this article is about, and it is the right tool once the state grows. The binary structure is compact and restores doubles bit for bit, but it holds only fixed-size members, so strings and dynamic arrays stay out. The binary and flat-file articles both write over the live file in place. The binary one checks only a version field on load, and neither verifies the content with a checksum.

The flat-file article is open about its own weak point. Its reliability analysis names a crash between truncating the file and finishing the write as the primary failure mode. It suggests a temporary file and a rename as the mitigation, a technique it expected to require a DLL. This article implements that mitigation with the native FileMove function, with no DLL, in a small, dependency-free class.

For a developer, the article builds that class step by step. Each save writes a temporary file and only then renames it over the live one. A header with a magic word, a version and a checksum lets the load reject a file whose pairs were truncated or changed. The store also keeps double arrays, so model weights and running statistics survive a restart along with counters.

This is a technical article, not a trading strategy and not financial advice. It delivers a reusable utility, and the demo advisor does not trade.


The save that corrupts itself

Consider the plain approach. You open the state file for writing, you write your keys and values, and you close it. It works for months. Then one save is interrupted: the terminal is killed in the middle of the write. The machine may lose power, or the process may be terminated during a platform update. The file is now half written. The old content is already gone, because opening for writing truncated it, and the new content never finished. On the next start, the advisor reads a truncated file and loads invalid values, or loads nothing at all.

This failure is worse than a missing file. A persistence tool exists to make an advisor more reliable, and in this failure mode it does the opposite. It hands back a corrupted state that looks valid enough to parse, and the advisor acts on it. A martingale step reads as zero when it should be four, and the next order is sized as if the streak never happened.

It is tempting to think that flushing after each write closes the gap, but it does not. A flush writes out the data written so far; it cannot write the part that was never produced. If the process dies after the first half of the record is written, a flush can only have written that half, which is the corrupted file we are trying to avoid. The problem is not caching. Overwriting a file in place has an unavoidable window in which the file is neither the old version nor the new one, and as long as the live file is the thing being written, that window exists.

The fix is an old and well-understood technique. You never write in place. You write the new content to a temporary file, and only after it is written and closed do you rename it over the real one. Our code never writes into the live file; the only step that touches it is one final FileMove call. The FileMove documentation does not describe how an existing file is replaced. How strictly that step behaves as a single operation depends on the operating system and the file system, and a network drive may give weaker guarantees than a local disk. So the claim here is practical rather than absolute: the pattern protects the live file from a partial overwrite. If the process dies while the temporary file is being written, the real state file was never touched, and the advisor resumes from the last good save. The temporary file may be left behind as a stale copy, but it is never mistaken for the real state, and the next successful save overwrites it. The image below shows both paths side by side.

One boundary belongs here. This protects against the process dying mid-write, the case this store is built for, because all the writing goes to the temporary file and the live file is only touched by the final replacement. A sudden power loss is a harder problem. The file system may record the rename before the data of the temporary file reach the physical disk, and then, after the restart, the live file can be incomplete. The load rejects it and the advisor starts empty. Because the store keeps no backup, a power loss can cost the whole state, not only the most recent save. The code calls FileFlush before closing the temporary file. The documentation says it writes the buffered data to disk, but it does not say whether the data go past the operating system cache onto the physical disk. A store like this is crash-safe, not a database with full durability guarantees. After a process crash during the write, the live file is not left half written, and that is a smaller promise than power-loss durability.

statestore_temp_rename

In-place writing leaves a half-written file if the process dies mid-write. Writing to a temporary file and renaming it into place means a crash during the write can only damage the temporary copy, not the live file.


Knowing when the file is wrong

Saving through a temporary file keeps an interrupted save from leaving a half-written live file. It does not protect against a file that was damaged by something else: an editor that mangled it, a disk error, a half-finished copy from a backup script. For that we need the file to carry a check on the integrity of its contents.

So the store writes a header line before the data. That header holds three things: a magic word so we know the file is ours, a version number so a future format can be read or rejected on purpose, and a checksum of the body. On load, the store reads the header, parses the body, recomputes the checksum, and compares. If they disagree, the file is not trusted. The store reports the mismatch and returns empty, which lets the advisor fall back to a clean start instead of trading on a damaged state.

The version number is justified in a series like this. This part writes a simple format, but a later part will want to store more: a timestamp of the last trade, a schema for the pending orders it tracks. When that day comes, newer code can detect files written by older versions and either upgrade them or ignore them, instead of parsing them and expecting fields they do not contain. Without a version field, new code would read an older file as if it were in the new format, with no warning.

The checksum does not need to be cryptographic. It only needs to catch accidental damage, so a small classic string hash is enough and it stays fully native. It catches a truncated write or a stray edit to a key or a value, not deliberate tampering, and a short hash like this can in principle collide. Keeping the whole thing as readable text pays off here too. When something does go wrong, you can open the state file in any editor and see what the advisor thought it knew, which key held which value at the moment it was saved. The binary structure cited in the introduction is smaller, but it cannot be read in an editor.

statestore_format

The file the demo wrote in a Strategy Tester run. The header carries a magic word, a version, and a checksum of the body. On load the pairs are parsed, and the body is rebuilt from them and hashed again. A lost line or a changed key or value makes the load reject the file instead of using it as if it were valid.


Beyond single numbers

The recent key-value store keeps scalars, one number or one string per key. That covers a counter or a flag, but it does not cover the thing many modern advisors most want to keep: an array. The weights of a model trained during the session, a rolling window of recent returns, a table of levels. If persistence only holds scalars, the one piece of state that is expensive to rebuild has to be split into one key per element or packed into a string by hand. A binary structure can hold an array only at a length fixed in its declaration.

So the store adds double arrays as first-class values. A double array is serialized to a line of space-separated numbers and read back into an array of the same length. This part implements the double array specifically, because it is the type that carries model weights and running statistics. An int, long, or bool array would follow the same text pattern, and Part 2 adds the ones it needs. This is what lets the store hold the weights of a logistic model or a small network. The logistic regression class in Machine Learning in Pure MQL5 (Part 1) saves its weights, means and deviations to a text file with a plain overwrite. Kept in CStateStore as double arrays, the same values would get a save through a temporary file and a checksum. CLogReg exposes only its weights and bias, through Weight() and Bias(), and has no setters, so using the store needs a small addition to the class.

Running statistics are the same story. An advisor that adapts to recent volatility often keeps a rolling mean and variance, updated tick by tick. Rebuilding those from history on every restart is slow, and the result is subtly different from the numbers it had before the interruption. When they are saved as an array, they come back at the precision they were written with, because each value is stored as text to a fixed number of decimal places rather than bit for bit. The state that takes hours or thousands of ticks to accumulate is often a vector rather than a single scalar. A store with a native array type saves it in one call instead of leaving the packing to every advisor.

statestore_types

The store keeps the usual scalars, and it also keeps arrays, which is what lets an advisor persist the weights of a small trained model rather than only counters and flags.


The class

The store is a self-contained class with three parts: an in-memory list of keys and values, typed accessors on top of it, and a save through a temporary file with an integrity check. Here is the surface.

//+------------------------------------------------------------------+
//| Crash-safe typed key-value state store                           |
//+------------------------------------------------------------------+
class CStateStore
  {
private:
   string            m_file;             // state file name, inside MQL5\Files
   string            m_keys[];           // keys, parallel to m_vals
   string            m_vals[];           // values as text, parallel to m_keys

   int               Find(const string key);                   // index of a key, or -1
   void              Put(const string key, const string val);  // insert or replace a pair
   string            Body(void);                               // rebuild the canonical text body
   uint              Checksum(const string s);                 // integrity hash of the body
public:
   //--- start unbound to any file, then bind the store to a file name
                     CStateStore(void);
   bool              Init(const string filename);
   //--- scalar values in: an integer, a double, a string, a boolean
   void              SetInt(const string key, const long value);
   void              SetDouble(const string key, const double value, const int digits = 10);
   void              SetString(const string key, const string value);
   void              SetBool(const string key, const bool value);
   //--- scalar values out, each returning the default when the key is absent
   long              GetInt(const string key, const long def = 0);
   double            GetDouble(const string key, const double def = 0.0);
   string            GetString(const string key, const string def = "");
   bool              GetBool(const string key, const bool def = false);
   //--- double arrays, to persist model weights and running statistics
   void              SetDoubleArray(const string key, const double &arr[], const int digits = 10);
   int               GetDoubleArray(const string key, double &arr[]);
   //--- inspect and reset the in-memory pairs (Clear does not touch the file)
   bool              Has(const string key);
   int               Count(void);
   void              Clear(void);
   //--- write a temp file and move it over the live file; read and verify
   bool              Save(void);
   bool              Load(void);
  };
//+------------------------------------------------------------------+
//| Constructor: start unbound to any file                           |
//+------------------------------------------------------------------+
CStateStore::CStateStore(void)
  {
   m_file = "";
  }
//+------------------------------------------------------------------+
//| Has: true if the key is stored                                   |
//+------------------------------------------------------------------+
bool CStateStore::Has(const string key)
  {
   return(Find(key) >= 0);
  }
//+------------------------------------------------------------------+
//| Count: number of stored key-value pairs                          |
//+------------------------------------------------------------------+
int CStateStore::Count(void)
  {
   return(ArraySize(m_keys));
  }

Setting a value is a lookup and a text conversion. Every type ends up as a string, because a text file can be opened and compared in any editor. The lookup replaces the value if the key already exists, so setting the same key twice leaves a single entry, and the file never grows a duplicate key.

//+------------------------------------------------------------------+
//| Put: set a key to a value, replacing it if it already exists     |
//+------------------------------------------------------------------+
void CStateStore::Put(const string key, const string val)
  {
   int i = Find(key);
   if(i >= 0)
     {
      m_vals[i] = val;
      return;
     }
   int n = ArraySize(m_keys);
   ArrayResize(m_keys, n + 1);
   ArrayResize(m_vals, n + 1);
   m_keys[n] = key;
   m_vals[n] = val;
  }

The body is built the same way every time, one key and value per line, so the checksum is deterministic. The save builds this string from the pairs in memory and the load rebuilds it from the parsed pairs, which is what makes the integrity check meaningful. The hash is the classic djb2 recurrence, a small loop and no dependency.

The load does not hash the raw bytes of the file. It parses the pairs, rebuilds the canonical body from them, and hashes that. Any change to a key or a value, and any lost line, changes the rebuilt body, so the check fails except in the rare case of a hash collision mentioned above. Changes that leave the parsed pairs as they were do not.

The parser skips an empty line and a line with no equals sign or one that starts with it, so those pass. An exact copy of an existing line placed after the original also passes, because Put stores the same value again. A copy placed above the original moves that key forward in the rebuilt body, so it fails. The header itself is not covered either: a damaged version that still reads as 1 or lower, or extra text after the checksum field, is not detected.

Even an untouched file does not hash to the stored number byte for byte, because the file holds \r\n line ends while the hash runs over the rebuilt text with \n. The checksum verifies the content the advisor will use, not the file.

//+------------------------------------------------------------------+
//| Body: the canonical body, one key=value line per pair, in order  |
//+------------------------------------------------------------------+
string CStateStore::Body(void)
  {
   string s = "";
   for(int i = 0; i < ArraySize(m_keys); i++)
      s += m_keys[i] + "=" + m_vals[i] + "\n";
   return(s);
  }
//+------------------------------------------------------------------+
//| Checksum: a small native hash (djb2). No library, no dependency  |
//+------------------------------------------------------------------+
uint CStateStore::Checksum(const string s)
  {
   uint h = 5381;
   int  n = StringLen(s);
   for(int i = 0; i < n; i++)
      h = ((h << 5) + h) + (uint)StringGetCharacter(s, i);
   return(h);
  }

The save is where the idea becomes code. It builds the body, computes its checksum, writes the header and the body to a temporary file, calls FileFlush, and only then moves the temporary file over the real one with FileMove and the FILE_REWRITE flag. That move is the only step that touches the live file, which is why a process crash while the temporary file is being written leaves the live file as it was.

//+------------------------------------------------------------------+
//| Save: write a temp file with a versioned, checksummed header,    |
//| then rename it over the live file. A process crash during the    |
//| write leaves the live file as it was.                            |
//+------------------------------------------------------------------+
bool CStateStore::Save(void)
  {
   if(m_file == "")
      return(false);
   string body = Body();
   string tmp  = m_file + ".tmp";
   int h = FileOpen(tmp, FILE_WRITE | FILE_TXT | FILE_ANSI);
   if(h == INVALID_HANDLE)
      return(false);
//--- text mode: FileWriteString stores each \n as \r\n: the header line, then one line per pair
   FileWriteString(h, STATESTORE_MAGIC + " " + IntegerToString(STATESTORE_VERSION) + " " +
                   IntegerToString((long)Checksum(body)) + "\n");
   FileWriteString(h, body);
   FileFlush(h);                 // push the bytes to disk before the rename
   FileClose(h);
//--- only this move touches the live file; all the writing went to the temp file
   return(FileMove(tmp, 0, m_file, FILE_REWRITE));
  }

The load is its mirror, with the integrity check in the middle. It reads the header, refuses a file that is not ours, parses the body into keys and values, and then recomputes the checksum over the rebuilt body. If it does not match the number in the header, the store empties itself and reports the problem, so the caller gets a clean empty state rather than a plausible wrong one.

The reading loop relies on text mode. Both sides open the file with FILE_TXT and FILE_ANSI. In that mode, FileWriteString adds the missing carriage return before every line feed, so each line ends in \r\n on disk. FileReadString then reads from the current position up to that \r\n. Both behaviors are documented. The documentation does not say in so many words that the returned line comes without the \r\n, so it was checked in the Strategy Tester. The demo wrote its file in one run, and the next run loaded it and resumed with the same 48 bars and 8 returns. That load would fail the checksum if the terminator stayed in the values. Together these make the loop correct: the first call returns the header, each following call returns one key=value pair, and FileIsEnding stops the loop at the end of the file. This is also why keys and values must not contain a newline, as the limitations below note.

//+------------------------------------------------------------------+
//| Load: read and verify. Returns false on a missing or unreadable  |
//| file, a header that is not ours, a newer format version, or a    |
//| checksum mismatch, leaving the store empty so the caller can     |
//| fall back to a clean start.                                      |
//+------------------------------------------------------------------+
bool CStateStore::Load(void)
  {
   Clear();
   if(m_file == "" || !FileIsExist(m_file))
      return(false);
   int h = FileOpen(m_file, FILE_READ | FILE_TXT | FILE_ANSI);
   if(h == INVALID_HANDLE)
      return(false);

//--- text mode: each FileReadString call returns one line, up to its \r\n
   string header = FileReadString(h);
   string hp[];
   int hn = StringSplit(header, ' ', hp);
   if(hn < 3 || hp[0] != STATESTORE_MAGIC)
     {
      FileClose(h);
      return(false);
     }
   if((int)StringToInteger(hp[1]) > STATESTORE_VERSION)   // a newer format this build cannot read
     {
      FileClose(h);
      Print("StateStore: file version is newer than this build supports, refusing to load");
      return(false);
     }
   uint storedCs = (uint)StringToInteger(hp[2]);

   while(!FileIsEnding(h))
     {
      string line = FileReadString(h);
      int eq = StringFind(line, "=");
      if(eq > 0)
         Put(StringSubstr(line, 0, eq), StringSubstr(line, eq + 1));
     }
   FileClose(h);

   if(Checksum(Body()) != storedCs)
     {
      Clear();                 // corrupted: do not hand the caller half a state
      Print("StateStore: checksum mismatch, the file is damaged or holds a value the format cannot store");
      return(false);
     }
   return(true);
  }

The array accessors are the same text idea, applied to a list. A double array becomes a line of space-separated numbers, and reading it back splits on the space and parses each element, returning the count so the caller knows how many it got.

//+------------------------------------------------------------------+
//| SetDoubleArray: store an array as space-separated numbers        |
//+------------------------------------------------------------------+
void CStateStore::SetDoubleArray(const string key, const double &arr[], const int digits = 10)
  {
   string s = "";
   int n = ArraySize(arr);
   for(int i = 0; i < n; i++)
     {
      if(i > 0)
         s += " ";
      s += DoubleToString(arr[i], digits);
     }
   Put(key, s);
  }
//+------------------------------------------------------------------+
//| GetDoubleArray: read a double array back, returning its size     |
//+------------------------------------------------------------------+
int CStateStore::GetDoubleArray(const string key, double &arr[])
  {
   int i = Find(key);
   if(i < 0 || m_vals[i] == "")
     {
      ArrayResize(arr, 0);
      return(0);
     }
   string parts[];
   int n = StringSplit(m_vals[i], ' ', parts);
   ArrayResize(arr, n);
   for(int k = 0; k < n; k++)
      arr[k] = StringToDouble(parts[k]);
   return(n);
  }


Seeing it survive

The demo advisor does nothing but keep state, so the persistence is the only thing under test. It counts every new bar it processes, and it keeps a rolling window of the last few bar returns as an array. On every bar it saves both, and on start it loads them back. It does not trade.

//+------------------------------------------------------------------+
//| OnInit: load any saved state, so the advisor resumes             |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   if(!g_store.Init(InpStateFile))
      return(INIT_FAILED);
   ArrayResize(g_window, 0);
   if(g_store.Load() && g_store.Has("bars"))
     {
      g_bars = g_store.GetInt("bars");
      g_lastBar = (datetime)g_store.GetInt("last_bar");   // do not count the saved bar again
      int n = g_store.GetDoubleArray("window", g_window);
      PrintFormat("Resumed: %I64d bars, %d returns in the window, last saved %s.",
                  g_bars, n, g_store.GetString("last_saved", "?"));
     }
   else
      Print("No saved state found (or the file was corrupted). Starting fresh.");
   return(INIT_SUCCEEDED);
  }

The saving is one call, done on every bar rather than only on shutdown. This matters, because a crash never calls OnDeinit. A tool that only saves on a clean exit does not survive the case it was built for, because the store exists for the exit that was not clean. Saving every bar, through a temporary file that protects the live file on each save, is what protects the state.

Saving on every bar costs one small file write and one rename, on the first tick of a new bar rather than on every tick. If an advisor keeps a large state, save on a schedule instead of on every bar. Because each save goes through a temporary file, saving often adds no risk of a half-written live file after a process crash.

//+------------------------------------------------------------------+
//| Persist: write the whole state to disk through the temp file     |
//+------------------------------------------------------------------+
void Persist(void)
  {
   g_store.SetInt("bars", g_bars);
   g_store.SetInt("last_bar", (long)g_lastBar);   // so a restart in the same bar does not count it again
   g_store.SetDoubleArray("window", g_window, 8);
   g_store.SetString("last_saved", TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES));
   if(!g_store.Save())
      Print("StateStore: save failed, state not persisted on this bar");
  }

Attach it, let a few bars form, then remove it and attach it again, or restart the terminal entirely. The advisor prints the count it resumed from and the size of the window it recovered, and the on-chart comment keeps climbing from where it was rather than resetting to zero. The counter and the array both come back: a scalar and an array cross a restart together.


Files and how to run it

The class is the deliverable; the demo advisor exercises it.

File What it holds
StateStore.mqh The reusable CStateStore class. Its saves write a temporary file and then rename it into place. A versioned and checksummed header makes the load reject a lost line or a changed key or value, and double arrays let model weights and running statistics persist along with scalars.
StateStoreDemo.mq5 A demo advisor that keeps a bar counter and a rolling window of returns, saves both on every bar, and reloads them on start so it resumes after a restart or a process crash. It does not trade.

To see it survive:

  1. Put StateStore.mqh and StateStoreDemo.mq5 in the same MQL5\Experts folder and compile.
  2. Attach the advisor to any chart, let a few bars form, then remove it and attach it again, or restart the terminal entirely.
  3. Watch the Experts tab and the chart comment: the bar count and the recovered window come back instead of resetting to zero, and the bar that was already counted is not counted again.
  4. To test the crash path, end the terminal process from the Windows Task Manager while the advisor runs, so OnDeinit is never called. Start the terminal again, attach the advisor if the chart does not show it, and it resumes from the save made on the last bar.
  5. To test the checksum, close the terminal, change one value in MQL5\Files\statestoredemo.txt by hand, and start it again. The Experts tab reports a checksum mismatch and the advisor starts fresh. Keep the file's encoding and its Windows line endings when you save it; an editor that adds a byte-order mark makes the load fail on the header instead, without the checksum message.
  6. To reuse the class in your own advisor, give it a file name in Init, call Save when your state changes, and call Load in OnInit.


Limitations

  1. Every save rewrites the whole file, which is O(n) in the number of keys. For a handful of values that is nothing, but for thousands of rows that change independently the native SQLite engine is the right tool instead, and the self-healing series cited in the introduction is the reference for that path.
  2. It assumes a single writer. Two advisors pointed at the same file will overwrite each other, and because the temporary file name is fixed they also collide on the .tmp, since there is no file lock.
  3. It keeps everything in memory, so it is meant for the tens or low hundreds of values an advisor typically carries, not for a large dataset.
  4. It is crash-safe, not durable in the database sense. The rename protects the live file against a process crash mid-write. A power loss shortly after a save can cost the most recent save. If the rename reaches the disk before the data do, it can cost the whole state, because the live file is left incomplete and the load rejects it. The FileMove documentation does not say how the replacement is carried out. How strictly it behaves as one operation depends on the operating system and the file system, and a network drive may not give the same guarantee. A crash during the FileMove call itself is covered only as far as the platform carries out the replacement as one step.
  5. It detects corruption but does not recover it. A bad file yields an empty state, not a rollback, because the store keeps no backup of the last good version. The checksum catches accidental damage, a truncated write or a changed key or value, not deliberate tampering. It verifies the parsed content, not the raw bytes, so a line the parser skips, or an exact copy of a line placed after the original, passes.
    Load returns false for every failure: a missing or unreadable file, a foreign or newer header, or a checksum mismatch, and the caller cannot tell them apart. The demo then starts fresh and saves over the file on its next bar. Copy a damaged file aside before restarting if you want to inspect it, and in your own advisor consider saving only after a successful Load or when no file exists yet.
    Save also does not read the temporary file back before the rename. If a write fails, for example on a full disk, a short file can replace the last good one, and the next load rejects it, so the previous state is lost.
  6. The on-disk format is one key=value line per pair, with no escaping. A key must not be empty or contain an equals sign, and neither a key nor a value may contain a newline. A value may contain an equals sign, because the load splits each line at the first one. The file is written as FILE_ANSI in the system code page, so keep keys and string values in plain ASCII.
    Save does not check any of this. A character the code page cannot represent is stored as a substitute, and a newline splits the pair; usually the rebuilt body then no longer matches the checksum, and the whole file is rejected on every load. If the text after the newline contains an equals sign, it is read back as an extra key, the rebuilt body is identical, and the checksum does not catch it. A key that contains an equals sign behaves the same way: it is read back as a shorter key holding the rest of the line.
  7. Doubles are stored with DoubleToString, whose digits argument counts places after the decimal point, so a value comes back rounded to that many places, not bit for bit. A very small value, such as a variance near 1e-8, keeps only a few significant digits at the default of 10. Raise digits when you need more, up to the 16 that DoubleToString accepts; a larger value falls back to 8 places.
  8. In the Strategy Tester, files live in the testing agent's own folder. A file written by one test can still be there when the next test or optimization pass runs on the same agent, so a test can resume from the state an earlier test ended with. An advisor that should start clean in every test can skip Load when MQLInfoInteger(MQL_TESTER) returns true.


What Part 2 adds

With a crash-safe state store in place, Part 2 takes on the problem that depends on it most: recovering the exact set of pending orders and their roles after a restart. A grid or a recovery scheme then picks up its own positions instead of double-counting them, on the same foundation built here.


Conclusion

A crash-safe state store is a small component that protects against one common class of silent failures, the file left half written by a process crash. Give it a file name. Call Save when the state changes and Load in OnInit. After a restart, an update, or a process crash during a save, the advisor resumes from its last save instead of a half-written file. The engineering behind it, a write-then-rename save and a self-verifying file, is deliberately simple, because persistence should be a part you set up once and then rarely revisit, within the limits listed above.

A final note: this is an educational article about a reusable utility, not a trading strategy and not financial advice. The value is the native, dependency-free code and a persistence layer for restarts and process crashes, with the boundaries listed above kept in view.

Attached files |
StateStore.mqh (13.85 KB)
StateStoreDemo.mq5 (5.39 KB)
Building a Divergence System (Part IV): Creating a Reusable Divergence Engine for MQL5 Building a Divergence System (Part IV): Creating a Reusable Divergence Engine for MQL5
The article extracts the series' divergence logic into DivergenceEngine.mqh, a reusable header for MQL5 indicators and Expert Advisors. It details the struct-based design, oscillator options (MPO4 or RSI), pivot and state handling, and the minimal access API. A Parabolic SAR EA demonstrates integration by adapting the acceleration factor from the detected divergence, providing a clear pattern you can reuse without duplicating code.
Symbolic Fourier Approximation in MQL5: Benchmarking SFA Against SAX Symbolic Fourier Approximation in MQL5: Benchmarking SFA Against SAX
We implement Symbolic Fourier Approximation in MQL5 and compare it to SAX under a shared harness on identical price windows. SFA keeps low‑frequency Fourier coefficients and learns per‑position bins (MCB), with a proven, sound lower bound. The measurements show how the same bit budget behaves under different splits of word length and alphabet, and give a practical rule for choosing settings for your symbol.
Neural Networks in Trading: Robust Trading Signals in Any Market Regime (Attention Modules) Neural Networks in Trading: Robust Trading Signals in Any Market Regime (Attention Modules)
In this article, we continue implementing the ST-Expert framework approaches, focusing on the practical aspects of applying them using MQL5. Earlier, we examined the theoretical foundations and key components of the model; now we move on to working directly with graph attention algorithms and local and global attention distribution. The main goal of this work is to demonstrate how ST-Expert's conceptual ideas are transformed into workable solutions for analyzing and forecasting financial time series.
Uncertainty as a Model (Part 2): Dependence Among Random Variables — From Correlation to Copulas Uncertainty as a Model (Part 2): Dependence Among Random Variables — From Correlation to Copulas
The second part of the series examines the mathematical framework for multivariate random variables, which is necessary for analyzing the dependence and joint behavior of market assets. This section describes joint distribution functions, the concepts of marginal and conditional distributions, and the conditions for dependence and independence of variables. The theoretical material is based on extending the analogy between probability and mass to multidimensional space. Particular attention is given to measures of association: from classical linear covariance and correlation to modern tools such as copulas and Shannon mutual information.