mirror of
https://github.com/FXDavid-OffbeatForex/MBT.git
synced 2026-11-02 03:56:12 +00:00
- 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>
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
"""
|
|
Feature-outcome analysis: do the indicator's own entry features separate
|
|
winners from losers? Uses the logged condition values (no recalculation) joined
|
|
to each signal's backtested outcome.
|
|
|
|
If a feature is predictive, win rate should trend across its buckets.
|
|
If win rate is flat across every feature, the entries have no edge to tune.
|
|
"""
|
|
import csv
|
|
from datetime import datetime
|
|
from core.connection import signal_file_path
|
|
from core.backtest import run_backtest
|
|
|
|
# 1. Outcomes per signal (independent mode = every signal evaluated, baseline exits)
|
|
rep = run_backtest(sequential=False)
|
|
outcome_by_time = {t.time.strftime("%Y-%m-%d %H:%M"): t.outcome for t in rep.trades}
|
|
|
|
# 2. Raw logged feature values
|
|
COLS = ["time","direction","entry","sl","tp","atr_pips","regime",
|
|
"adx","mom_fast","mom_med","mom_slow","vol_ratio","vpt_dir","near_level"]
|
|
rows = []
|
|
with open(signal_file_path(), encoding="ansi") as f:
|
|
for r in csv.reader(f):
|
|
if not r or len(r) < 12: continue
|
|
if not r[0][:4].isdigit(): continue # skip header
|
|
d = dict(zip(COLS, r))
|
|
try:
|
|
t = datetime.strptime(d["time"], "%Y.%m.%d %H:%M").strftime("%Y-%m-%d %H:%M")
|
|
except ValueError:
|
|
continue
|
|
oc = outcome_by_time.get(t)
|
|
if oc not in ("WIN","LOSS"): continue
|
|
rows.append({
|
|
"win": 1 if oc == "WIN" else 0,
|
|
"direction": d["direction"],
|
|
"adx": float(d["adx"]),
|
|
"mom_fast": abs(float(d["mom_fast"])), # magnitude in signal direction
|
|
"mom_med": abs(float(d["mom_med"])),
|
|
"mom_slow": abs(float(d["mom_slow"])),
|
|
"vol_ratio": float(d["vol_ratio"]),
|
|
"hour": int(d["time"][11:13]),
|
|
})
|
|
|
|
n = len(rows)
|
|
base_wr = sum(r["win"] for r in rows) / n * 100
|
|
print(f"signals analysed: {n} baseline win rate: {base_wr:.1f}%\n")
|
|
|
|
def buckets(feature, k=5):
|
|
vals = sorted(r[feature] for r in rows)
|
|
cuts = [vals[int(len(vals)*i/k)] for i in range(1, k)]
|
|
def b(v):
|
|
for i, c in enumerate(cuts):
|
|
if v <= c: return i
|
|
return k-1
|
|
groups = [[] for _ in range(k)]
|
|
for r in rows:
|
|
groups[b(r[feature])].append(r["win"])
|
|
print(f"=== {feature} (quintiles, low -> high) ===")
|
|
for i, g in enumerate(groups):
|
|
if not g: continue
|
|
wr = sum(g)/len(g)*100
|
|
lo = "min" if i==0 else f"{cuts[i-1]:.3f}"
|
|
hi = "max" if i==k-1 else f"{cuts[i]:.3f}"
|
|
bar = "#" * int(wr)
|
|
print(f" Q{i+1} [{lo:>7}..{hi:>7}] n={len(g):>4} WR={wr:5.1f}% {bar}")
|
|
print()
|
|
|
|
for feat in ["adx","mom_fast","mom_med","mom_slow","vol_ratio","hour"]:
|
|
buckets(feat)
|
|
|
|
# direction split
|
|
for dval in ("LONG","SHORT"):
|
|
g = [r["win"] for r in rows if r["direction"]==dval]
|
|
if g:
|
|
print(f"direction {dval}: n={len(g)} WR={sum(g)/len(g)*100:.1f}%")
|