mirror of
https://github.com/FXDavid-OffbeatForex/MBT.git
synced 2026-11-02 03:56:12 +00:00
Audit fixes: public repo readiness + MCP improvements
- Add MIT LICENSE - Anonymize broker path in config.example.yaml; add slippage_points + risk_per_trade - Fix encoding ansi→cp1252 in signals.py (cross-platform safe) - Fix install.py silently ignoring pip errors - Cap get_ohlcv at 2000 bars; add ping tool; clarify HTML report UX in descriptions - Escape symbol/timeframe in HTML report to prevent malformed output - Move run_*.py to scripts/; move example CSVs to examples/ - Update .gitignore to block accidental CSV commits from root - README: Python 3.9+ requirement, Chart.js internet note, new sections for examples/ and scripts/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
f47ac228b1
commit
3490441649
@@ -9,3 +9,8 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
|
# signal CSVs — example files in examples/ are tracked; any CSV dropped in
|
||||||
|
# the root or scripts/ is personal data and should not be committed
|
||||||
|
/*.csv
|
||||||
|
/scripts/*.csv
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 davidcyberdash
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
**Backtest any MetaTrader 5 indicator by talking to Claude.**
|
**Backtest any MetaTrader 5 indicator by talking to Claude.**
|
||||||
|
|
||||||
|
**Requirements:** Python 3.9+ · MetaTrader 5 (Windows) · Claude Code
|
||||||
|
|
||||||
Drop one include into your indicator, and MBT lets you fetch live data, read
|
Drop one include into your indicator, and MBT lets you fetch live data, read
|
||||||
your indicator's signals, and run a full backtest with an HTML report — all
|
your indicator's signals, and run a full backtest with an HTML report — all
|
||||||
through Claude, no scripting.
|
through Claude, no scripting.
|
||||||
@@ -129,8 +131,31 @@ a timestamp.
|
|||||||
|
|
||||||
| Tool | Purpose |
|
| Tool | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `get_ohlcv` | live OHLCV bars for any symbol/timeframe |
|
| `ping` | check MT5 is running and reachable |
|
||||||
|
| `get_ohlcv` | live OHLCV bars for any symbol/timeframe (max 2000) |
|
||||||
| `get_signals` | read your indicator's logged signals |
|
| `get_signals` | read your indicator's logged signals |
|
||||||
| `backtest` | replay + full metrics + HTML report |
|
| `backtest` | replay + full metrics + HTML report (requires internet for chart) |
|
||||||
| `validate_signals` | check SL/TP geometry of every signal |
|
| `validate_signals` | check SL/TP geometry of every signal |
|
||||||
| `get_config` | show the active terminal + signal file |
|
| `get_config` | show the active terminal + signal file |
|
||||||
|
|
||||||
|
> **Note:** the HTML report is written to `reports/` on your machine and opened in a
|
||||||
|
> browser. Claude cannot open it directly — it will give you the file path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
`examples/` contains sample signal CSVs you can use to try MBT without attaching
|
||||||
|
an indicator. Point `signal_file` in `config.yaml` to any of them:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
signal_file: "C:/abs/path/to/MBT/examples/test_signals.csv"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Research scripts
|
||||||
|
|
||||||
|
`scripts/` contains one-off analysis scripts used during strategy development.
|
||||||
|
They are not part of the core toolkit and have no documentation — treat them as
|
||||||
|
reference material, not user tools.
|
||||||
|
|||||||
+9
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# Full path to the terminal64.exe of the MT5 instance your indicator runs on.
|
# Full path to the terminal64.exe of the MT5 instance your indicator runs on.
|
||||||
# Right-click your MT5 shortcut -> Properties -> Target to find it.
|
# Right-click your MT5 shortcut -> Properties -> Target to find it.
|
||||||
mt5_path: "C:/Users/Administrator/Desktop/Ava Trade MT5 Terminal/terminal64.exe"
|
mt5_path: "C:/Path/To/Your/terminal64.exe"
|
||||||
|
|
||||||
# The signal file your indicator writes (via SignalLogger.mqh).
|
# The signal file your indicator writes (via SignalLogger.mqh).
|
||||||
# Either a bare filename (looked up inside the MT5 .../MQL5/Files folder)
|
# Either a bare filename (looked up inside the MT5 .../MQL5/Files folder)
|
||||||
@@ -22,3 +22,11 @@ ambiguous_bar: "loss"
|
|||||||
|
|
||||||
# Where generated HTML reports are written (relative to the MBT folder).
|
# Where generated HTML reports are written (relative to the MBT folder).
|
||||||
reports_dir: "reports"
|
reports_dir: "reports"
|
||||||
|
|
||||||
|
# Spread + slippage allowance in points, used to estimate transaction cost per trade.
|
||||||
|
# 5 is a reasonable default for most forex pairs; increase for gold/indices.
|
||||||
|
slippage_points: 5.0
|
||||||
|
|
||||||
|
# Risk per trade as a decimal (0.01 = 1%) — used only to translate R units into
|
||||||
|
# account-percentage returns in the backtest report. Does not affect WIN/LOSS outcomes.
|
||||||
|
risk_per_trade: 0.01
|
||||||
|
|||||||
+5
-2
@@ -5,6 +5,7 @@ Uses Chart.js from a CDN so the output is a single self-contained .html the
|
|||||||
user can open in any browser or screenshot for content.
|
user can open in any browser or screenshot for content.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import html as _html
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ def render(rep: BacktestReport, filename: str = None) -> str:
|
|||||||
|
|
||||||
path = os.path.join(reports_dir(), filename)
|
path = os.path.join(reports_dir(), filename)
|
||||||
|
|
||||||
|
sym = _html.escape(rep.symbol)
|
||||||
|
tf = _html.escape(rep.timeframe)
|
||||||
pf_str = "∞" if rep.profit_factor == float("inf") else f"{rep.profit_factor:.2f}"
|
pf_str = "∞" if rep.profit_factor == float("inf") else f"{rep.profit_factor:.2f}"
|
||||||
|
|
||||||
cards = "".join([
|
cards = "".join([
|
||||||
@@ -79,7 +82,7 @@ def render(rep: BacktestReport, filename: str = None) -> str:
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>MBT Backtest — {rep.symbol} {rep.timeframe}</title>
|
<title>MBT Backtest — {sym} {tf}</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body {{ background:#0d1117; color:#e6edf3; font-family:-apple-system,Segoe UI,Roboto,sans-serif;
|
body {{ background:#0d1117; color:#e6edf3; font-family:-apple-system,Segoe UI,Roboto,sans-serif;
|
||||||
@@ -102,7 +105,7 @@ def render(rep: BacktestReport, filename: str = None) -> str:
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Backtest Report — {rep.symbol} {rep.timeframe}</h1>
|
<h1>Backtest Report — {sym} {tf}</h1>
|
||||||
<div class="sub">{rep.total} trades taken · {rep.signals_skipped} signals skipped (trade already open)
|
<div class="sub">{rep.total} trades taken · {rep.signals_skipped} signals skipped (trade already open)
|
||||||
· generated {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
· generated {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||||
· ambiguous bars counted as losses (conservative)</div>
|
· ambiguous bars counted as losses (conservative)</div>
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ def load_signals(path: Optional[str] = None) -> List[Signal]:
|
|||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
path = path or signal_file_path()
|
path = path or signal_file_path()
|
||||||
|
|
||||||
with open(path, encoding="ansi", newline="") as f:
|
with open(path, encoding="cp1252", errors="replace", newline="") as f:
|
||||||
rows = list(csv.reader(f))
|
rows = list(csv.reader(f))
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
|
|||||||
+4
-2
@@ -25,8 +25,10 @@ def step(msg):
|
|||||||
|
|
||||||
def install_deps():
|
def install_deps():
|
||||||
step("Installing Python dependencies")
|
step("Installing Python dependencies")
|
||||||
subprocess.run([sys.executable, "-m", "pip", "install", "-r",
|
result = subprocess.run([sys.executable, "-m", "pip", "install", "-r",
|
||||||
os.path.join(ROOT, "requirements.txt")], check=False)
|
os.path.join(ROOT, "requirements.txt")])
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("WARNING: pip install failed. Check the error above before continuing.")
|
||||||
|
|
||||||
|
|
||||||
def make_config():
|
def make_config():
|
||||||
|
|||||||
+28
-3
@@ -15,7 +15,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
from core.connection import load_config, signal_file_path
|
from core.connection import load_config, connect, signal_file_path
|
||||||
from core.ohlcv import fetch_recent
|
from core.ohlcv import fetch_recent
|
||||||
from core.signals import load_signals
|
from core.signals import load_signals
|
||||||
from core.backtest import run_backtest, report_to_dict
|
from core.backtest import run_backtest, report_to_dict
|
||||||
@@ -29,7 +29,9 @@ def get_ohlcv(symbol: str, timeframe: str, count: int = 100) -> dict:
|
|||||||
"""
|
"""
|
||||||
Fetch live OHLCV bars from the configured MT5 terminal (newest first).
|
Fetch live OHLCV bars from the configured MT5 terminal (newest first).
|
||||||
timeframe: 1m 5m 15m 30m 1h 4h 1d 1w. Generic — works for any symbol.
|
timeframe: 1m 5m 15m 30m 1h 4h 1d 1w. Generic — works for any symbol.
|
||||||
|
count: max 2000 bars.
|
||||||
"""
|
"""
|
||||||
|
count = min(count, 2000)
|
||||||
try:
|
try:
|
||||||
bars = fetch_recent(symbol, timeframe, count)
|
bars = fetch_recent(symbol, timeframe, count)
|
||||||
return {"symbol": symbol, "timeframe": timeframe, "count": len(bars), "bars": bars}
|
return {"symbol": symbol, "timeframe": timeframe, "count": len(bars), "bars": bars}
|
||||||
@@ -75,7 +77,9 @@ def backtest(since_date: str = None, html_report: bool = True) -> dict:
|
|||||||
profit factor, expectancy, drawdown, per-regime breakdown) in R units.
|
profit factor, expectancy, drawdown, per-regime breakdown) in R units.
|
||||||
|
|
||||||
since_date: optional 'YYYY-MM-DD' filter on signals.
|
since_date: optional 'YYYY-MM-DD' filter on signals.
|
||||||
html_report: also write a standalone HTML report with an equity curve.
|
html_report: write a standalone HTML report to reports/ and return its path.
|
||||||
|
The report requires an internet connection (loads Chart.js from CDN).
|
||||||
|
Claude cannot open the file — share the path with the user to open in a browser.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
sigs = load_signals()
|
sigs = load_signals()
|
||||||
@@ -124,7 +128,8 @@ def validate_signals() -> dict:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def get_config() -> dict:
|
def get_config() -> dict:
|
||||||
"""Show the active MBT configuration (which terminal and signal file are in use)."""
|
"""Show the active MBT configuration (which terminal and signal file are in use).
|
||||||
|
Note: the output includes local file paths — do not share it publicly."""
|
||||||
try:
|
try:
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
return {"config": cfg, "resolved_signal_file": signal_file_path()}
|
return {"config": cfg, "resolved_signal_file": signal_file_path()}
|
||||||
@@ -132,5 +137,25 @@ def get_config() -> dict:
|
|||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def ping() -> dict:
|
||||||
|
"""Check whether the MT5 terminal is running and reachable. Use this first
|
||||||
|
if any other tool returns an error, to confirm the connection is alive."""
|
||||||
|
try:
|
||||||
|
connect()
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
info = mt5.terminal_info()
|
||||||
|
if info is None:
|
||||||
|
return {"connected": False, "error": "terminal_info() returned None"}
|
||||||
|
return {
|
||||||
|
"connected": True,
|
||||||
|
"terminal": info.name,
|
||||||
|
"build": info.build,
|
||||||
|
"trade_allowed": info.trade_allowed,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"connected": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mcp.run(transport="stdio")
|
mcp.run(transport="stdio")
|
||||||
|
|||||||
Reference in New Issue
Block a user