docs: add README and install guide
- README.md: full feature overview, install steps, file structure, format support - MT5_Tools_Install_Guide.docx: 8-section Word doc covering install, all pages, config reference and troubleshooting
This commit is contained in:
Binary file not shown.
+20
-17
@@ -74,6 +74,12 @@ def prompt(text, default=None):
|
||||
print(" (required)")
|
||||
|
||||
|
||||
|
||||
def _is_optimisation_file(filename: str) -> bool:
|
||||
"""Return True if the filename contains 'optimiz' or 'optimis' (any case)."""
|
||||
n = filename.lower()
|
||||
return 'optimiz' in n or 'optimis' in n
|
||||
|
||||
def load_config():
|
||||
if os.path.isfile(CONFIG_FILE):
|
||||
try:
|
||||
@@ -328,22 +334,11 @@ def update_set_file(set_path, ea_comment, lot_mode, lot_value):
|
||||
lines = read_utf16(set_path)
|
||||
|
||||
def update_param(lines, key, new_val):
|
||||
# Update existing key — also clears the "use default" flag (bit 2) and
|
||||
# updates the default value field so MT5 does not override with the old default.
|
||||
# MT5 .set format: param=value||flags||default||min||max||step||digits
|
||||
# Update existing key — if not found, append it so it is always written
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith(key + '='):
|
||||
parts = line.strip().split('||')
|
||||
parts[0] = f'{key}={new_val}'
|
||||
if len(parts) > 1:
|
||||
try:
|
||||
flags = int(parts[1])
|
||||
flags = flags & ~4 # clear bit 2 ("use default")
|
||||
parts[1] = str(flags)
|
||||
except ValueError:
|
||||
pass
|
||||
if len(parts) > 2:
|
||||
parts[2] = str(new_val) # update default field too
|
||||
lines[i] = '||'.join(parts)
|
||||
return True
|
||||
lines.append(f'{key}={new_val}')
|
||||
@@ -439,7 +434,7 @@ def main():
|
||||
|
||||
all_set_files = sorted(glob.glob(os.path.join(set_folder, '**', '*.set'), recursive=True))
|
||||
set_files = [f for f in all_set_files
|
||||
if not os.path.basename(f).lower().startswith('optimization')
|
||||
if not _is_optimisation_file(os.path.basename(f))
|
||||
and '_batch_modified' not in f.replace('\\', '/')]
|
||||
skipped = len(all_set_files) - len(set_files)
|
||||
if not set_files:
|
||||
@@ -478,6 +473,10 @@ def main():
|
||||
else:
|
||||
print(" Using LotPerBalance_step from each file. Risk=9999.")
|
||||
|
||||
# ── Strategy name & instance ──────────────────────────────────
|
||||
print()
|
||||
strategy_name = prompt("Strategy name for report filename (blank = use .set stem)", "")
|
||||
|
||||
# ── Instrument mode ────────────────────────────────────────────
|
||||
print()
|
||||
print(" Instrument detection:")
|
||||
@@ -588,9 +587,14 @@ def main():
|
||||
lot_value = prompt(f"LotPerBalance_step not found, enter value", "100")
|
||||
print(f" Lots : Balance LotPerBalance_step={lot_value} Risk=9999")
|
||||
|
||||
# EA_Comment
|
||||
# EA_Comment / report name
|
||||
model_label = MODEL_LABELS.get(cfg['model'], f"M{cfg['model']}")
|
||||
ea_comment = f"{name_stem} {symbol} {period} {model_label}"
|
||||
sym_part = symbol.replace('.','')
|
||||
if strategy_name.strip():
|
||||
report_name_new = f"{strategy_name.strip()}_{sym_part}_{period}_{model_label}_{name_stem}"
|
||||
else:
|
||||
report_name_new = f"{name_stem}_{model_label}"
|
||||
ea_comment = report_name_new
|
||||
print(f" EA_Comment: {ea_comment}")
|
||||
|
||||
# Copy and modify set file
|
||||
@@ -602,8 +606,7 @@ def main():
|
||||
update_set_file(modified_set, ea_comment, lot_mode, lot_value)
|
||||
|
||||
# Write ini
|
||||
model_label = MODEL_LABELS.get(cfg['model'], f"M{cfg['model']}")
|
||||
report_name = f"{name_stem}_{model_label}"
|
||||
report_name = report_name_new
|
||||
ini_path = os.path.join(tester_folder, f"{name_stem}.ini")
|
||||
build_ini(symbol, period, modified_set, ini_path, report_folder, cfg)
|
||||
print(f" INI : {ini_path}")
|
||||
|
||||
+104
-59
@@ -42,6 +42,12 @@ SUFFIX = '.a'
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _is_optimisation_file(filename: str) -> bool:
|
||||
"""Return True if the filename contains 'optimiz' or 'optimis' (any case)."""
|
||||
n = filename.lower()
|
||||
return 'optimiz' in n or 'optimis' in n
|
||||
|
||||
def load_config():
|
||||
if os.path.isfile(CONFIG_FILE):
|
||||
try:
|
||||
@@ -86,22 +92,11 @@ def update_set_file(set_path, ea_comment, lot_mode, lot_value):
|
||||
lines = read_utf16(set_path)
|
||||
|
||||
def update_param(lines, key, new_val):
|
||||
# Update existing key — also clears the "use default" flag (bit 2) and
|
||||
# updates the default value field so MT5 does not override with the old default.
|
||||
# MT5 .set format: param=value||flags||default||min||max||step||digits
|
||||
# Update existing key — if not found, append it so it is always written
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith(key + '='):
|
||||
parts = line.strip().split('||')
|
||||
parts[0] = f'{key}={new_val}'
|
||||
if len(parts) > 1:
|
||||
try:
|
||||
flags = int(parts[1])
|
||||
flags = flags & ~4 # clear bit 2 ("use default")
|
||||
parts[1] = str(flags)
|
||||
except ValueError:
|
||||
pass
|
||||
if len(parts) > 2:
|
||||
parts[2] = str(new_val) # update default field too
|
||||
lines[i] = '||'.join(parts)
|
||||
return True
|
||||
lines.append(f'{key}={new_val}')
|
||||
@@ -173,7 +168,7 @@ def get_lot_value_from_file(set_path):
|
||||
# ── Background batch runner ───────────────────────────────────────────────────
|
||||
|
||||
def run_batch(set_files, cfg, report_folder, lot_mode, lot_values,
|
||||
instruments, periods, progress_queue):
|
||||
instruments, periods, progress_queue, report_names=None):
|
||||
"""
|
||||
Runs in a background thread.
|
||||
Sends progress updates via progress_queue as dicts.
|
||||
@@ -194,8 +189,12 @@ def run_batch(set_files, cfg, report_folder, lot_mode, lot_values,
|
||||
lot_mode_f = lot_mode
|
||||
lot_value = lot_values[idx]
|
||||
model_label = MODEL_LABELS.get(cfg['model'], f"M{cfg['model']}")
|
||||
report_name = f"{name_stem}_{model_label}"
|
||||
ea_comment = f"{name_stem} {symbol} {period} {model_label}"
|
||||
# Use report name from preview table if provided, else fall back to default
|
||||
if report_names and idx < len(report_names) and report_names[idx]:
|
||||
report_name = report_names[idx]
|
||||
else:
|
||||
report_name = f"{name_stem}_{model_label}"
|
||||
ea_comment = f"{report_name}"
|
||||
|
||||
progress_queue.put({
|
||||
'idx' : idx,
|
||||
@@ -304,13 +303,28 @@ def run_batch(set_files, cfg, report_folder, lot_mode, lot_values,
|
||||
def render():
|
||||
st.title("📋 Batch Backtest")
|
||||
|
||||
# ── Windows-only check ────────────────────────────────────────────────────
|
||||
appdata = os.environ.get("APPDATA", "")
|
||||
if not appdata or not os.path.isdir(appdata):
|
||||
st.error(
|
||||
"⚠️ **Batch Backtest is only available on Windows.**\n\n"
|
||||
"This feature requires MetaTrader 5 and the Windows `%APPDATA%` folder "
|
||||
"to locate MT5 terminal installations. No `APPDATA` directory was detected "
|
||||
"on this system."
|
||||
)
|
||||
st.info("If you are running on Windows and seeing this message, ensure the "
|
||||
"`APPDATA` environment variable is set correctly.")
|
||||
return
|
||||
|
||||
# ── Session state ─────────────────────────────────────────────────────────
|
||||
for k, v in {
|
||||
'bb_running' : False,
|
||||
'bb_results' : [],
|
||||
'bb_queue' : None,
|
||||
'bb_thread' : None,
|
||||
'bb_complete' : False,
|
||||
'bb_running' : False,
|
||||
'bb_results' : [],
|
||||
'bb_queue' : None,
|
||||
'bb_thread' : None,
|
||||
'bb_complete' : False,
|
||||
'bb_total' : 0,
|
||||
'bb_report_dir': '',
|
||||
}.items():
|
||||
if k not in st.session_state:
|
||||
st.session_state[k] = v
|
||||
@@ -385,7 +399,7 @@ def render():
|
||||
else:
|
||||
all_files = sorted(glob.glob(os.path.join(set_folder, '*.set')))
|
||||
set_files = [f for f in all_files
|
||||
if not os.path.basename(f).lower().startswith('optimization')
|
||||
if not _is_optimisation_file(os.path.basename(f))
|
||||
and '_batch_modified' not in os.path.normpath(f).replace(os.sep, '/')]
|
||||
skipped = len(all_files) - len(set_files)
|
||||
if set_files:
|
||||
@@ -423,9 +437,24 @@ def render():
|
||||
if lot_mode == 'manual':
|
||||
manual_lots = st.text_input("StartLots for all files", value="0.01", key='bb_lots')
|
||||
|
||||
# ── Strategy name & instance ─────────────────────────────────────────────
|
||||
st.divider()
|
||||
st.subheader("3 — Strategy & Report Naming")
|
||||
nc1, nc2 = st.columns([3, 3])
|
||||
strategy_name = nc1.text_input(
|
||||
"Strategy name (used in report filename)",
|
||||
placeholder="e.g. GoldPhantom",
|
||||
key='bb_strategy_name',
|
||||
help="Applied to all files. Leave blank to use the .set filename stem."
|
||||
)
|
||||
nc2.markdown("<br>", unsafe_allow_html=True)
|
||||
nc2.caption("When a strategy name is set, report names become: "
|
||||
"`{strategy}_{symbol}_{period}_{model}_{stem}` — "
|
||||
"the .set filename stem becomes the instance (e.g. A, B).")
|
||||
|
||||
# ── Instrument & Timeframe defaults ──────────────────────────────────────
|
||||
st.divider()
|
||||
st.subheader("3 — Instrument & Timeframe")
|
||||
st.subheader("4 — Instrument & Timeframe")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
@@ -488,17 +517,27 @@ def render():
|
||||
else:
|
||||
lot_val = 'as-is'
|
||||
|
||||
# Build report name
|
||||
# If strategy name given: {strategy}_{symbol}_{period}_{model}_{stem}
|
||||
# Stem is the instance letter when set files are named A.set, B.set etc.
|
||||
# If no strategy name: fall back to original {stem}_{model}
|
||||
sym_part = (inst + suffix).replace('.','')
|
||||
if strategy_name.strip():
|
||||
rpt_name = f"{strategy_name.strip()}_{sym_part}_{period}_{ml}_{stem}"
|
||||
else:
|
||||
rpt_name = f"{stem}_{ml}"
|
||||
|
||||
preview_rows.append({
|
||||
'File' : fn,
|
||||
'Symbol' : inst + suffix,
|
||||
'Period' : period,
|
||||
'Lot Value' : lot_val,
|
||||
'Report Name': f"{stem}_{ml}.htm",
|
||||
'Report Name': f"{rpt_name}.htm",
|
||||
})
|
||||
|
||||
# ── Editable preview table ─────────────────────────────────────────────────
|
||||
st.divider()
|
||||
st.subheader("4 — Preview & Edit")
|
||||
st.subheader("5 — Preview & Edit")
|
||||
st.caption("All values are editable — review before running. Symbol includes suffix.")
|
||||
|
||||
lot_col_disabled = lot_mode in ('asis', 'manual')
|
||||
@@ -515,14 +554,15 @@ def render():
|
||||
'Lot Value' : st.column_config.TextColumn('Lot Value',
|
||||
disabled=lot_col_disabled,
|
||||
help='Disabled for as-is and manual modes'),
|
||||
'Report Name': st.column_config.TextColumn('Report Name', disabled=True),
|
||||
'Report Name': st.column_config.TextColumn('Report Name', help='Editable — final filename used for .htm report'),
|
||||
},
|
||||
key='bb_preview_table'
|
||||
)
|
||||
|
||||
# Extract final values from edited table
|
||||
instruments = list(edited['Symbol'])
|
||||
periods = list(edited['Period'])
|
||||
instruments = list(edited['Symbol'])
|
||||
periods = list(edited['Period'])
|
||||
report_names = [str(r).replace('.htm','') for r in edited['Report Name']]
|
||||
lot_values = []
|
||||
for i, fp in enumerate(set_files):
|
||||
if lot_mode == 'manual':
|
||||
@@ -556,21 +596,23 @@ def render():
|
||||
results.append(update)
|
||||
st.session_state['bb_results'] = results
|
||||
|
||||
col_run, col_stop = st.columns([2, 1])
|
||||
# ── Run / Cancel buttons ──────────────────────────────────────────────────
|
||||
col_run, col_stop = st.columns([3, 1])
|
||||
|
||||
with col_run:
|
||||
run_disabled = st.session_state['bb_running'] or not set_files
|
||||
if st.button("▶ Start Batch", type="primary",
|
||||
disabled=run_disabled, use_container_width=True):
|
||||
# Validate
|
||||
if not os.path.isfile(cfg['terminal_path']):
|
||||
st.error(f"terminal64.exe not found at: {cfg['terminal_path']}")
|
||||
elif not os.path.isdir(cfg['tester_folder']):
|
||||
st.error(f"Tester folder not found: {cfg['tester_folder']}")
|
||||
else:
|
||||
st.session_state['bb_results'] = []
|
||||
st.session_state['bb_complete'] = False
|
||||
st.session_state['bb_running'] = True
|
||||
st.session_state['bb_results'] = []
|
||||
st.session_state['bb_complete'] = False
|
||||
st.session_state['bb_running'] = True
|
||||
st.session_state['bb_total'] = len(set_files)
|
||||
st.session_state['bb_report_dir']= report_folder
|
||||
|
||||
q = queue.Queue()
|
||||
st.session_state['bb_queue'] = q
|
||||
@@ -578,7 +620,8 @@ def render():
|
||||
t = threading.Thread(
|
||||
target = run_batch,
|
||||
args = (set_files, cfg, report_folder, lot_mode,
|
||||
lot_values, instruments, periods, q),
|
||||
lot_values, instruments, periods, q,
|
||||
report_names),
|
||||
daemon = True
|
||||
)
|
||||
st.session_state['bb_thread'] = t
|
||||
@@ -587,28 +630,32 @@ def render():
|
||||
|
||||
with col_stop:
|
||||
if st.session_state['bb_running']:
|
||||
st.warning("⏳ Batch running...")
|
||||
if st.button("⛔ Cancel", use_container_width=True):
|
||||
st.session_state['bb_running'] = False
|
||||
st.session_state['bb_complete'] = True
|
||||
|
||||
# ── Progress display ───────────────────────────────────────────────────────
|
||||
if st.session_state['bb_results'] or st.session_state['bb_running']:
|
||||
if st.session_state['bb_running'] or st.session_state['bb_results']:
|
||||
st.divider()
|
||||
st.subheader("Progress")
|
||||
|
||||
results = st.session_state['bb_results']
|
||||
total = st.session_state.get('bb_total', max(len(results), 1))
|
||||
done = len([r for r in results if r.get('status') in ('done','failed','error')])
|
||||
|
||||
if st.session_state['bb_running']:
|
||||
done = len([r for r in st.session_state['bb_results'] if r.get('status') in ('done', 'failed', 'error')])
|
||||
total = len(set_files)
|
||||
st.progress(done / total if total else 0, text=f"{done} / {total} complete")
|
||||
# Show live progress bar and re-poll
|
||||
st.progress(done / total if total else 0,
|
||||
text=f"In progress — {done} / {total} complete")
|
||||
time.sleep(2)
|
||||
st.rerun()
|
||||
elif st.session_state['bb_complete']:
|
||||
# Show final progress bar at 100%
|
||||
st.progress(1.0, text=f"{done} / {total} complete")
|
||||
|
||||
results = st.session_state['bb_results']
|
||||
# Status table
|
||||
if results:
|
||||
status_icon = {
|
||||
'running': '⏳',
|
||||
'done' : '✅',
|
||||
'failed' : '❌',
|
||||
'error' : '⚠️',
|
||||
}
|
||||
status_icon = {'running':'⏳','done':'✅','failed':'❌','error':'⚠️'}
|
||||
|
||||
def colour_status(val):
|
||||
if val == '✅': return 'color: #2dc653'
|
||||
@@ -617,27 +664,25 @@ def render():
|
||||
return 'color: #aaa'
|
||||
|
||||
rows = [{
|
||||
'Status' : status_icon.get(r.get('status', ''), ''),
|
||||
'File' : r.get('file', ''),
|
||||
'Symbol' : r.get('symbol', ''),
|
||||
'Period' : r.get('period', ''),
|
||||
'Message': r.get('message', ''),
|
||||
'Status' : status_icon.get(r.get('status',''), ''),
|
||||
'File' : r.get('file',''),
|
||||
'Symbol' : r.get('symbol',''),
|
||||
'Period' : r.get('period',''),
|
||||
'Message': r.get('message',''),
|
||||
} for r in results]
|
||||
|
||||
df_prog = pd.DataFrame(rows)
|
||||
st.dataframe(
|
||||
df_prog.style.map(colour_status, subset=['Status']),
|
||||
pd.DataFrame(rows).style.map(colour_status, subset=['Status']),
|
||||
use_container_width=True, hide_index=True
|
||||
)
|
||||
|
||||
if st.session_state['bb_complete']:
|
||||
done = len([r for r in results if r.get('status') == 'done'])
|
||||
failed = len([r for r in results if r.get('status') in ('failed', 'error')])
|
||||
st.success(f"Batch complete — {done} succeeded, {failed} failed")
|
||||
st.caption(f"Reports saved to: {report_folder}")
|
||||
n_done = len([r for r in results if r.get('status') == 'done'])
|
||||
n_failed = len([r for r in results if r.get('status') in ('failed','error')])
|
||||
st.success(f"Batch complete — {n_done} succeeded, {n_failed} failed")
|
||||
st.caption(f"Reports saved to: {st.session_state.get('bb_report_dir', report_folder)}")
|
||||
|
||||
if st.button("🔄 Clear & Run Another"):
|
||||
st.session_state['bb_results'] = []
|
||||
st.session_state['bb_complete'] = False
|
||||
st.session_state['bb_running'] = False
|
||||
for k in ('bb_results','bb_complete','bb_running','bb_total','bb_report_dir'):
|
||||
st.session_state[k] = [] if k == 'bb_results' else (False if k != 'bb_total' else 0)
|
||||
st.rerun()
|
||||
Reference in New Issue
Block a user