mirror of
https://github.com/FXDavid-OffbeatForex/MBT.git
synced 2026-11-02 03:56:12 +00:00
feat: run_indicator — backtest an indicator headlessly, no chart attach
MT5 can't attach an indicator to a chart via API, so a generic host EA (MBT_IndicatorHost) loads any indicator with iCustom inside the Strategy Tester; the indicator's OnCalculate runs over the date range and its SignalLogger.mqh (now tester-aware: FILE_COMMON under MQL_TESTER) writes the signals to the shared Common/Files folder, which the new run_indicator tool reads back. Removes the manual "attach to a chart" step from the flow. - core/indicator_runner.py: drives the headless run (stale-guard, .set in Profiles/Tester, signal span + warm-up note, instance-clash detection) - mcp_server.py: wires the run_indicator tool - mql5/SignalLogger.mqh: tester-gated FILE_COMMON (chart behaviour unchanged) - mql5/MBT_IndicatorHost.mq5: the generic host EA (places no trades) - install.py: also copies the host EA into MQL5/Experts - README: no-chart flow documented; chart attach now optional Validated under Wine: 348 signals on XAUUSDzero H1, stale-guard + error paths confirmed; host EA + SignalLogger compile 0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a4b0fbf615
commit
ef1b66756b
@@ -38,6 +38,7 @@ That's what MBT is built on. The indicator logs what it actually did. MBT reads
|
||||
│ tools exposed to Claude: │
|
||||
│ get_ohlcv │
|
||||
│ get_signals │
|
||||
│ run_indicator (headless) │
|
||||
│ backtest (+ HTML report) │
|
||||
│ validate_signals │
|
||||
└────────────────────────────┘
|
||||
@@ -137,8 +138,16 @@ LogSignal(shift, false, entry, sl, tp, "TRENDING"); // SELL
|
||||
`regime` is optional — pass `""` if you don't use it. When present, the backtest
|
||||
report breaks results down per regime.
|
||||
|
||||
Compile, attach to the chart. The indicator writes `signals.csv` to
|
||||
`<terminal>\MQL5\Files\`.
|
||||
Compile it — that's all. From here you have two ways to make the indicator log
|
||||
its signals:
|
||||
|
||||
- **Headless (recommended):** ask Claude to run it — `run_indicator` computes the
|
||||
indicator over your chosen date range with no chart and writes the signals
|
||||
automatically. Nothing to attach.
|
||||
- **On a chart:** attach the compiled indicator to a chart as usual; it writes
|
||||
`signals.csv` live as it runs.
|
||||
|
||||
Either way you end up with the same signal file, which the `backtest` tool reads.
|
||||
|
||||
---
|
||||
|
||||
@@ -146,7 +155,23 @@ Compile, attach to the chart. The indicator writes `signals.csv` to
|
||||
|
||||
Just describe what you want in Claude Code — Claude picks the right tool automatically.
|
||||
|
||||
**Backtest your indicator's signals:**
|
||||
**Run your indicator and backtest it — no chart needed:**
|
||||
|
||||
> "run my RegimePlusePro indicator on XAUUSD H1 for the last 3 years, then backtest it"
|
||||
|
||||
Claude runs the indicator headlessly (it computes over the whole range and logs
|
||||
its own signals), then replays those signals on real bars and hands you the
|
||||
metrics + an HTML report. You never open a chart.
|
||||
|
||||
> **One-time setup for the headless run:** it needs the `tester:` block filled in
|
||||
> (see `config.example.yaml`) and the bundled host EA `MBT_IndicatorHost.mq5`
|
||||
> (in `mql5/`) compiled into `MQL5/Experts` — just ask Claude to "compile the MBT
|
||||
> indicator host" once. The indicator must log via `SignalLogger.mqh` (a bespoke
|
||||
> logger that writes only to the local Files folder won't be found under the
|
||||
> tester), and it runs with its **default** inputs.
|
||||
|
||||
**Or backtest signals you already have** (from a previous run, or an indicator
|
||||
running live on a chart):
|
||||
|
||||
> "backtest my signals"
|
||||
> "backtest signals since 2026-01-01 and give me the HTML report"
|
||||
@@ -230,6 +255,7 @@ a timestamp.
|
||||
| `validate_signals` | check SL/TP geometry of every signal |
|
||||
| `get_config` | show the active terminal + signal file |
|
||||
| `compile_ea` | compile an EA/indicator with MetaEditor → structured errors/warnings |
|
||||
| `run_indicator` | run an indicator headlessly so it logs its signals — no chart attach |
|
||||
| `run_strategy_tester` | run MT5's real headless Strategy Tester on an EA → metrics + report |
|
||||
| `signal_parity` | diff two signal sets and report the first divergence |
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
Headless indicator runner — make an indicator log its signals with no chart.
|
||||
|
||||
MT5 has no API to attach an indicator to a chart, and its Strategy Tester only
|
||||
runs Experts. So MBT ships a tiny generic host EA (MBT_IndicatorHost) that loads
|
||||
ANY indicator via iCustom inside the tester; once loaded the indicator's
|
||||
OnCalculate runs bar-by-bar over the chosen date range, and if it logs its own
|
||||
signals (via SignalLogger.mqh) those signals get written automatically.
|
||||
|
||||
This module drives that: it writes the host EA's .set file (which indicator to
|
||||
load), clears any stale signal CSV, runs the tester headlessly, then reads back
|
||||
the signals the indicator wrote to the shared Common\\Files folder.
|
||||
|
||||
Requirement: the indicator must log through SignalLogger.mqh — under the tester
|
||||
that logger transparently writes to Common\\Files, which is where we read. An
|
||||
indicator with a bespoke logger that writes only to the local Files folder lands
|
||||
in the tester's per-agent sandbox and won't be found here.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
from .connection import reports_dir
|
||||
from .tester import (_tester_cfg, _data_dir, _common_files_dir, _launch_cmd,
|
||||
build_tester_ini, _fmt_date)
|
||||
|
||||
HOST_EA = "MBT_IndicatorHost"
|
||||
|
||||
|
||||
def _profiles_tester_dir() -> str:
|
||||
"""MT5 reads a tester .set file from MQL5/Profiles/Tester/ — return that dir
|
||||
(created if missing) under the active data dir."""
|
||||
d = os.path.join(_data_dir(), "MQL5", "Profiles", "Tester")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def _write_set_file(name: str, indicator: str) -> str:
|
||||
"""Write a minimal .set passing InpIndicator to the host EA. Returns the bare
|
||||
filename (ExpertParameters is resolved relative to Profiles/Tester)."""
|
||||
fname = name + ".set"
|
||||
path = os.path.join(_profiles_tester_dir(), fname)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("InpIndicator=%s\n" % indicator)
|
||||
f.write("InpBuffersToPull=2\n")
|
||||
return fname
|
||||
|
||||
|
||||
def _signal_csv_path(signal_file: str) -> str:
|
||||
"""Where the hosted indicator's SignalLogger writes under the tester: the
|
||||
shared Common/Files folder, by the file's bare name."""
|
||||
return os.path.join(_common_files_dir(), os.path.basename(signal_file))
|
||||
|
||||
|
||||
def _signal_span(path: str):
|
||||
"""Return (count, first_time, last_time) over the signal CSV's data rows
|
||||
(excluding a header). Times are the raw first-column strings, or None."""
|
||||
if not os.path.isfile(path):
|
||||
return 0, None, None
|
||||
n = 0
|
||||
first = last = None
|
||||
with open(path, encoding="cp1252", errors="replace") as f:
|
||||
for line in f:
|
||||
if not line.strip():
|
||||
continue
|
||||
if line.lower().startswith("time,"):
|
||||
continue # header (identified by content, not position)
|
||||
n += 1
|
||||
stamp = line.split(",", 1)[0].strip()
|
||||
if first is None:
|
||||
first = stamp
|
||||
last = stamp
|
||||
return n, first, last
|
||||
|
||||
|
||||
def run_indicator(indicator, symbol, timeframe="h1", from_date=None, to_date=None,
|
||||
signal_file="signals.csv", model=None, deposit=None,
|
||||
leverage=None, timeout_sec=None, host_ea=HOST_EA) -> dict:
|
||||
"""Run an indicator headlessly through the tester so it logs its own signals.
|
||||
|
||||
indicator : name under MQL5/Indicators (e.g. 'RegimePlusePro'), .ex5 optional.
|
||||
symbol : broker symbol (e.g. 'XAUUSD').
|
||||
timeframe : 1m 5m 15m 30m 1h 4h 1d 1w.
|
||||
from/to : 'YYYY-MM-DD' (omit -> broker's full history).
|
||||
signal_file : the indicator's SignalLogFile name; read back from Common/Files.
|
||||
Uses the indicator's compiled-in default, so leave as the default
|
||||
unless the indicator hard-codes a different name.
|
||||
|
||||
Returns the signal CSV path and the number of signals logged. The indicator
|
||||
runs with its DEFAULT inputs (iCustom uses compiled defaults) — if you tuned
|
||||
its inputs on a chart, recompile the indicator with those as defaults.
|
||||
"""
|
||||
t = _tester_cfg()
|
||||
model = model or t.get("default_model", "open_prices")
|
||||
deposit = deposit or t.get("default_deposit", 10000)
|
||||
leverage = leverage or t.get("default_leverage", 100)
|
||||
timeout = timeout_sec or t.get("timeout_sec", 1800)
|
||||
|
||||
indicator = str(indicator).strip()
|
||||
if not indicator:
|
||||
return {"error": "indicator name is empty."}
|
||||
|
||||
host_name = host_ea if host_ea.lower().endswith(".ex5") else host_ea + ".ex5"
|
||||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
run_name = "%s_%s" % (os.path.splitext(os.path.basename(indicator))[0], stamp)
|
||||
|
||||
# .set tells the host EA which indicator to load.
|
||||
set_file = _write_set_file(run_name, indicator)
|
||||
|
||||
ini_txt = build_tester_ini(host_name, symbol, timeframe, from_date, to_date,
|
||||
model, deposit, leverage, report_path=run_name,
|
||||
set_file=set_file)
|
||||
ini_path = os.path.join(reports_dir(), run_name + ".ini")
|
||||
with open(ini_path, "w", encoding="utf-8") as f:
|
||||
f.write(ini_txt)
|
||||
|
||||
# STALE GUARD: clear the signal CSV before launch, so a leftover file from a
|
||||
# previous run can never be read back as this run's signals. Fail loud if it
|
||||
# can't be cleared (a silent stale read is exactly what this prevents).
|
||||
csv_path = _signal_csv_path(signal_file)
|
||||
if os.path.isfile(csv_path):
|
||||
try:
|
||||
os.remove(csv_path)
|
||||
except OSError:
|
||||
pass
|
||||
if os.path.isfile(csv_path):
|
||||
return {"error": "Could not clear the stale signal file at %s "
|
||||
"(file locked?). Close any running MT5 terminal and "
|
||||
"retry — leaving it risks reporting the previous run's "
|
||||
"signals as this run's." % csv_path}
|
||||
|
||||
cmd = _launch_cmd(ini_path)
|
||||
t0 = time.time()
|
||||
timed_out = False
|
||||
try:
|
||||
subprocess.run(cmd, timeout=timeout,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
elapsed = round(time.time() - t0, 1)
|
||||
|
||||
count, first_sig, last_sig = _signal_span(csv_path)
|
||||
wrote = os.path.isfile(csv_path)
|
||||
|
||||
result = {
|
||||
"indicator": indicator,
|
||||
"host_ea": host_ea,
|
||||
"symbol": symbol,
|
||||
"timeframe": timeframe,
|
||||
"from": _fmt_date(from_date),
|
||||
"to": _fmt_date(to_date),
|
||||
"model": model,
|
||||
"signal_csv": csv_path if wrote else None,
|
||||
"signal_count": count,
|
||||
"first_signal": first_sig,
|
||||
"last_signal": last_sig,
|
||||
"ran_seconds": elapsed,
|
||||
"timed_out": timed_out,
|
||||
"command": " ".join(cmd),
|
||||
"ini": ini_path,
|
||||
"set_file": set_file,
|
||||
}
|
||||
|
||||
# The indicator computes over warm-up history the tester loads BEFORE
|
||||
# from_date, so it can log signals earlier than the requested start. Surface
|
||||
# that rather than let the wider span look like a bug.
|
||||
if count and from_date and first_sig and first_sig[:10].replace(".", "-") < _fmt_date(from_date).replace(".", "-"):
|
||||
result["note"] = ("signals start before from_date because the indicator also "
|
||||
"computed over the tester's warm-up history; filter by date "
|
||||
"downstream (e.g. backtest since_date) if you want only the "
|
||||
"requested window.")
|
||||
|
||||
if count == 0:
|
||||
# An instant return with nothing written usually means another terminal
|
||||
# already owns the data dir (MT5 is single-instance).
|
||||
if elapsed < 8 and not wrote:
|
||||
result["error"] = ("Terminal returned in %.1fs and wrote no signal file — "
|
||||
"another MT5 terminal is almost certainly already "
|
||||
"running on this data dir (single-instance). Close it "
|
||||
"and retry." % elapsed)
|
||||
else:
|
||||
result["error"] = ("No signals were logged. Check that: the indicator "
|
||||
"name is correct and compiled into MQL5/Indicators; "
|
||||
"it logs via SignalLogger.mqh (bespoke loggers that "
|
||||
"skip Common/Files land in the tester sandbox); the "
|
||||
"symbol/timeframe has history for the date range; and "
|
||||
"the strategy actually fires in that range.")
|
||||
return result
|
||||
+32
-3
@@ -8,7 +8,8 @@ It will:
|
||||
1. install Python dependencies
|
||||
2. create config.yaml from the example if missing
|
||||
3. copy SignalLogger.mqh into your MT5 MQL5\Include folder (if it can find it)
|
||||
4. print the exact `claude mcp add` command to register the server
|
||||
4. copy MBT_IndicatorHost.mq5 into MQL5\Experts (for headless run_indicator)
|
||||
5. print the exact `claude mcp add` command to register the server
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -66,6 +67,33 @@ def copy_include():
|
||||
print(f"No Include folders found. Manually copy {src} yourself.")
|
||||
|
||||
|
||||
def copy_host_ea():
|
||||
"""Copy the headless host EA into MQL5\\Experts so `run_indicator` works.
|
||||
Compile it once in MetaEditor (or ask Claude to) before first use."""
|
||||
step("MQL5 host EA (for headless run_indicator)")
|
||||
src = os.path.join(ROOT, "mql5", "MBT_IndicatorHost.mq5")
|
||||
base = os.path.join(os.environ.get("APPDATA", ""), "MetaQuotes", "Terminal")
|
||||
if not os.path.isdir(base):
|
||||
print(f"Could not auto-find the terminal folder. Manually copy {src} "
|
||||
f"into your terminal's MQL5\\Experts folder and compile it.")
|
||||
return
|
||||
copied = 0
|
||||
for d in os.listdir(base):
|
||||
exp = os.path.join(base, d, "MQL5", "Experts")
|
||||
if os.path.isdir(exp):
|
||||
try:
|
||||
shutil.copy(src, os.path.join(exp, "MBT_IndicatorHost.mq5"))
|
||||
print(f"Copied MBT_IndicatorHost.mq5 -> {exp}")
|
||||
copied += 1
|
||||
except Exception as e:
|
||||
print(f"Skipped {exp}: {e}")
|
||||
if copied:
|
||||
print("Compile it once (MetaEditor, or ask Claude to 'compile the MBT "
|
||||
"indicator host') before the first headless run.")
|
||||
else:
|
||||
print(f"No Experts folders found. Manually copy {src} yourself.")
|
||||
|
||||
|
||||
def print_mcp_cmd():
|
||||
step("Register the MCP server with Claude Code")
|
||||
server = os.path.join(ROOT, "mcp_server.py").replace("\\", "/")
|
||||
@@ -80,6 +108,7 @@ if __name__ == "__main__":
|
||||
install_deps()
|
||||
make_config()
|
||||
copy_include()
|
||||
copy_host_ea()
|
||||
print_mcp_cmd()
|
||||
print("\nDone. Edit config.yaml, attach your indicator (with SignalLogger), "
|
||||
"then ask Claude to backtest.")
|
||||
print("\nDone. Edit config.yaml, then ask Claude to run your indicator "
|
||||
"(with SignalLogger) and backtest it — no chart needed.")
|
||||
|
||||
@@ -24,6 +24,7 @@ from core.report_html import render as render_html
|
||||
from core.tester import run_strategy_tester as _run_tester
|
||||
from core.compiler import compile_mql5 as _compile_mql5
|
||||
from core.parity import signal_parity as _signal_parity
|
||||
from core.indicator_runner import run_indicator as _run_indicator
|
||||
|
||||
mcp = FastMCP("MBT — MT5 Backtest Toolkit")
|
||||
|
||||
@@ -188,6 +189,36 @@ def compile_ea(source: str) -> dict:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def run_indicator(indicator: str, symbol: str, timeframe: str = "1h",
|
||||
from_date: str = None, to_date: str = None,
|
||||
signal_file: str = "signals.csv") -> dict:
|
||||
"""
|
||||
Run an indicator headlessly so it logs its own signals — no chart attach.
|
||||
|
||||
MT5 can't attach an indicator to a chart programmatically, so this loads the
|
||||
indicator inside MT5's Strategy Tester (via a generic host EA) and lets it
|
||||
compute bar-by-bar over the date range. The indicator's own SignalLogger.mqh
|
||||
writes the signals, which this reads back. After it returns, run `backtest`
|
||||
to evaluate those signals.
|
||||
|
||||
indicator: name under MQL5/Indicators (e.g. 'RegimePlusePro'), .ex5 optional.
|
||||
symbol: broker symbol (e.g. 'XAUUSD').
|
||||
timeframe: 1m 5m 15m 30m 1h 4h 1d 1w.
|
||||
from_date / to_date: 'YYYY-MM-DD' (omit for the broker's full history).
|
||||
signal_file: the indicator's SignalLogFile name (default 'signals.csv').
|
||||
|
||||
Requires tester.* in config.yaml, the host EA + the indicator compiled, and
|
||||
the indicator to log via SignalLogger.mqh. The indicator runs with its DEFAULT
|
||||
inputs. Returns the signal CSV path and how many signals were logged.
|
||||
"""
|
||||
try:
|
||||
return _run_indicator(indicator, symbol, timeframe, from_date, to_date,
|
||||
signal_file=signal_file)
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def signal_parity(reference: str, candidate: str = None,
|
||||
price_tol: float = 0.0) -> dict:
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| MBT_IndicatorHost.mq5 — MBT (MT5 Backtest Toolkit) |
|
||||
//| |
|
||||
//| A generic, do-nothing Expert Advisor whose only job is to LOAD |
|
||||
//| another indicator inside MT5's Strategy Tester and make it run. |
|
||||
//| |
|
||||
//| Why this exists: |
|
||||
//| MT5's API can't attach an indicator to a chart, and the tester |
|
||||
//| only runs Experts — not indicators. But an Expert can load any |
|
||||
//| indicator with iCustom(), and once loaded the indicator's |
|
||||
//| OnCalculate() runs bar-by-bar over the test range. If that |
|
||||
//| indicator logs its own signals (via SignalLogger.mqh), those |
|
||||
//| signals get written with zero manual chart work. |
|
||||
//| |
|
||||
//| This EA places NO trades. It just hosts the indicator and pulls |
|
||||
//| one buffer value each bar so the terminal actually computes it |
|
||||
//| (a custom indicator in the tester is only calculated when its |
|
||||
//| data is requested). |
|
||||
//| |
|
||||
//| Driven by MBT's `run_indicator` tool, which passes the indicator |
|
||||
//| name through a .set file (InpIndicator). |
|
||||
//+------------------------------------------------------------------+
|
||||
#property strict
|
||||
#property version "1.00"
|
||||
#property description "MBT host EA — runs another indicator headlessly so it logs its signals. Places no trades."
|
||||
|
||||
// Indicator name relative to MQL5\Indicators (e.g. "RegimePlusePro" or
|
||||
// "Subfolder\\MyIndi"), with or without the .ex5 extension. Passed by the tool.
|
||||
input string InpIndicator = "";
|
||||
|
||||
// How many of the indicator's plotted buffers to request each bar. Requesting a
|
||||
// buffer is what forces the tester to calculate the indicator; one is enough for
|
||||
// almost every indicator, but a couple is cheap insurance.
|
||||
input int InpBuffersToPull = 2;
|
||||
|
||||
int g_handle = INVALID_HANDLE;
|
||||
datetime g_lastBar = 0;
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
int OnInit()
|
||||
{
|
||||
string name = InpIndicator;
|
||||
StringTrimLeft(name);
|
||||
StringTrimRight(name);
|
||||
if(name == "")
|
||||
{
|
||||
Print("MBT_IndicatorHost: InpIndicator is empty — nothing to host. ",
|
||||
"Pass the indicator name via the .set file.");
|
||||
return(INIT_PARAMETERS_INCORRECT);
|
||||
}
|
||||
// iCustom wants the name with no extension — strip a trailing .ex5/.mq5 if the
|
||||
// caller passed one (the tool documents the extension as optional).
|
||||
string lower = name;
|
||||
StringToLower(lower);
|
||||
if(StringFind(lower, ".ex5", StringLen(lower) - 4) >= 0 ||
|
||||
StringFind(lower, ".mq5", StringLen(lower) - 4) >= 0)
|
||||
name = StringSubstr(name, 0, StringLen(name) - 4);
|
||||
// iCustom resolves the name under MQL5\Indicators.
|
||||
g_handle = iCustom(_Symbol, _Period, name);
|
||||
if(g_handle == INVALID_HANDLE)
|
||||
{
|
||||
PrintFormat("MBT_IndicatorHost: could not load indicator '%s' (iCustom failed). "
|
||||
"Check the name/path under MQL5\\Indicators and that it compiled.", name);
|
||||
return(INIT_FAILED);
|
||||
}
|
||||
PrintFormat("MBT_IndicatorHost: hosting '%s' on %s %s.",
|
||||
name, _Symbol, EnumToString((ENUM_TIMEFRAMES)_Period));
|
||||
return(INIT_SUCCEEDED);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
void OnDeinit(const int reason)
|
||||
{
|
||||
if(g_handle != INVALID_HANDLE)
|
||||
IndicatorRelease(g_handle);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Once per new bar, request the hosted indicator's buffers so the |
|
||||
//| tester computes it for the just-closed bar. We never act on the |
|
||||
//| values — the indicator's own logging does the real work. |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnTick()
|
||||
{
|
||||
if(g_handle == INVALID_HANDLE)
|
||||
return;
|
||||
|
||||
datetime barTime = iTime(_Symbol, _Period, 0);
|
||||
if(barTime == g_lastBar)
|
||||
return; // still the same bar — pull once per bar
|
||||
g_lastBar = barTime;
|
||||
|
||||
double buf[];
|
||||
int pulls = (InpBuffersToPull < 1) ? 1 : InpBuffersToPull;
|
||||
for(int b = 0; b < pulls; b++)
|
||||
{
|
||||
// Best-effort: a buffer index the indicator doesn't have simply returns
|
||||
// <=0 copied; that's fine, the request still triggers calculation.
|
||||
CopyBuffer(g_handle, b, 0, 2, buf);
|
||||
}
|
||||
}
|
||||
//+------------------------------------------------------------------+
|
||||
+24
-5
@@ -19,13 +19,30 @@
|
||||
// Name of the CSV the toolkit reads. Must match `signal_file` in config.yaml.
|
||||
input string SignalLogFile = "signals.csv";
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Where the log is written. |
|
||||
//| |
|
||||
//| On a chart the file goes to the local <terminal>\MQL5\Files\ . |
|
||||
//| Under the Strategy Tester that folder is a per-agent sandbox the |
|
||||
//| toolkit can't find, so when running in the tester we transparently|
|
||||
//| switch to the shared Common\Files\ folder instead — which is |
|
||||
//| exactly where MBT's headless `run_indicator` tool reads from. |
|
||||
//| Every file call below ORs in this flag, so the two modes stay in |
|
||||
//| lockstep (a chart write that local-deletes won't fight a tester |
|
||||
//| write that common-deletes). |
|
||||
//+------------------------------------------------------------------+
|
||||
int _SigCommonFlag()
|
||||
{
|
||||
return MQLInfoInteger(MQL_TESTER) ? FILE_COMMON : 0;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Reset the log (call once in OnInit, or on full recalculation, |
|
||||
//| so the file always reflects the currently drawn signals). |
|
||||
//+------------------------------------------------------------------+
|
||||
void ResetSignalLog()
|
||||
{
|
||||
FileDelete(SignalLogFile);
|
||||
FileDelete(SignalLogFile, _SigCommonFlag());
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -33,8 +50,9 @@ void ResetSignalLog()
|
||||
//+------------------------------------------------------------------+
|
||||
int _OpenSignalLog()
|
||||
{
|
||||
bool isNew = !FileIsExist(SignalLogFile);
|
||||
int h = FileOpen(SignalLogFile, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI);
|
||||
int common = _SigCommonFlag();
|
||||
bool isNew = !FileIsExist(SignalLogFile, common);
|
||||
int h = FileOpen(SignalLogFile, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI|common);
|
||||
if(h == INVALID_HANDLE)
|
||||
return INVALID_HANDLE;
|
||||
|
||||
@@ -60,11 +78,12 @@ void LogSignal(int shift, bool isLong, double entry, double sl, double tp,
|
||||
string regime = "")
|
||||
{
|
||||
datetime barTime = iTime(_Symbol, _Period, shift);
|
||||
int common = _SigCommonFlag();
|
||||
|
||||
// --- dedup: skip if this bar's time is already the last logged row ---
|
||||
if(FileIsExist(SignalLogFile))
|
||||
if(FileIsExist(SignalLogFile, common))
|
||||
{
|
||||
int rh = FileOpen(SignalLogFile, FILE_READ|FILE_TXT|FILE_ANSI);
|
||||
int rh = FileOpen(SignalLogFile, FILE_READ|FILE_TXT|FILE_ANSI|common);
|
||||
if(rh != INVALID_HANDLE)
|
||||
{
|
||||
string lastLine = "";
|
||||
|
||||
Reference in New Issue
Block a user