Files
mql5-multisignal-dca-ccbsn/wave_strategy_optimizer.py
Phạm Phú Nguyễn Hưng 4cb86ecd5f feat: MQL5 MultiSignal DCA CCBSN EA - 9 signal modes + adaptive DCA + CCBSN partial close
- 9 indicator signal modes: Ichimoku, EMA Cross, RSI, BB Bounce, Stoch, CCI, MACD, Supertrend, Momentum
- DCA multi-tier with adaptive distance and lot multiplier
- CCBSN (partial close 50% -> breakeven -> trailing)
- Sniper (trim oldest losing orders)
- Anti-Detect for Prop Firm compliance
- Python ML optimizer for DCA parameters
- Wave strategy brute-force optimizer (2240+ combos)
- 85+ optimizable inputs for MT5 Strategy Tester
- Auto-detect filling type (Exness compatibility)
- Retry logic for order closing

Built by @hungpixi | Comarai.com
2026-03-17 04:16:24 +07:00

800 lines
32 KiB
Python

"""
Wave Strategy Optimizer v2.0 - Brute-force Multi-Indicator Confluence
=====================================================================
Thay vì Decision Tree, approach mới:
1. Compute nhiều indicators
2. Brute-force tìm CONFLUENCE (kết hợp) indicator nào cho win rate cao nhất
3. Backtest trực tiếp → chọn combo tốt nhất
4. Output MQ5 EA với rules cụ thể, kiểm chứng được
Author: Comarai (https://comarai.com)
"""
import argparse, csv, os, sys, math
from datetime import datetime
from collections import Counter
import numpy as np
PIP = 0.1 # XAUUSD
# =============================================================================
# DATA
# =============================================================================
def load_csv(filepath):
candles = []
with open(filepath, 'r', encoding='utf-8-sig') as f:
first = f.readline()
delimiter = '\t' if '\t' in first else ','
f.seek(0)
reader = csv.reader(f, delimiter=delimiter)
header = next(reader)
hl = [h.strip().lower() for h in header]
col = {}
for i, h in enumerate(hl):
if h in ('date', '<date>'): col['date'] = i
elif h in ('time', '<time>'): col['time'] = i
elif h in ('open', '<open>'): col['open'] = i
elif h in ('high', '<high>'): col['high'] = i
elif h in ('low', '<low>'): col['low'] = i
elif h in ('close', '<close>'): col['close'] = i
elif h in ('tickvol', '<tickvol>', 'volume'): col['volume'] = i
for row in reader:
try:
c = {'o': float(row[col['open']]), 'h': float(row[col['high']]),
'l': float(row[col['low']]), 'c': float(row[col['close']])}
if 'date' in col and 'time' in col:
ds = row[col['date']].strip()
ts = row[col['time']].strip()
for fmt in ('%Y.%m.%d %H:%M:%S', '%Y.%m.%d %H:%M'):
try: c['dt'] = datetime.strptime(ds + ' ' + ts, fmt); break
except ValueError: continue
c['vol'] = int(float(row[col.get('volume', col.get('open'))])) if 'volume' in col else 0
candles.append(c)
except: continue
print(f"[DATA] {len(candles)} candles")
return candles
# =============================================================================
# INDICATORS (computed as numpy arrays, all looking at COMPLETED bars like MQ5)
# =============================================================================
def calc_ema(data, period):
r = np.zeros(len(data)); k = 2.0/(period+1); r[0] = data[0]
for i in range(1, len(data)): r[i] = data[i]*k + r[i-1]*(1-k)
return r
def calc_sma(data, period):
r = np.zeros(len(data))
cs = np.cumsum(data)
r[period-1:] = (cs[period-1:] - np.concatenate([[0], cs[:-period]])) / period
return r
def calc_rsi(closes, period=14):
r = np.full(len(closes), 50.0)
d = np.diff(closes, prepend=closes[0])
g = np.where(d > 0, d, 0.0)
l = np.where(d < 0, -d, 0.0)
ag = np.zeros(len(closes)); al = np.zeros(len(closes))
if period < len(closes):
ag[period] = np.mean(g[1:period+1]); al[period] = np.mean(l[1:period+1])
for i in range(period+1, len(closes)):
ag[i] = (ag[i-1]*(period-1)+g[i])/period
al[i] = (al[i-1]*(period-1)+l[i])/period
for i in range(period, len(closes)):
if al[i] == 0: r[i] = 100.0
else: r[i] = 100.0 - 100.0/(1.0+ag[i]/al[i])
return r
def calc_atr(candles, period=14):
n = len(candles); r = np.zeros(n)
for i in range(1, n):
tr = max(candles[i]['h']-candles[i]['l'], abs(candles[i]['h']-candles[i-1]['c']), abs(candles[i]['l']-candles[i-1]['c']))
r[i] = (r[i-1]*(period-1)+tr)/period if i >= period else tr
return r
def calc_stoch(candles, k_per=14, d_per=3):
n = len(candles); k = np.full(n, 50.0)
for i in range(k_per-1, n):
hh = max(candles[j]['h'] for j in range(i-k_per+1, i+1))
ll = min(candles[j]['l'] for j in range(i-k_per+1, i+1))
if hh != ll: k[i] = (candles[i]['c']-ll)/(hh-ll)*100
d = calc_sma(k, d_per)
return k, d
def calc_bb(closes, period=20, mult=2.0):
mid = calc_sma(closes, period)
u = np.zeros(len(closes)); lo = np.zeros(len(closes))
for i in range(period-1, len(closes)):
s = np.std(closes[i-period+1:i+1])
u[i] = mid[i]+mult*s; lo[i] = mid[i]-mult*s
return u, mid, lo
def calc_macd(closes, f=12, s=26, sig=9):
ml = calc_ema(closes, f) - calc_ema(closes, s)
sl = calc_ema(ml, sig)
return ml, sl, ml-sl
# =============================================================================
# SIGNAL GENERATORS (each returns +1, -1, or 0 per bar)
# =============================================================================
def signal_ema_cross(candles, fast=9, slow=21):
"""EMA crossover signal"""
c = np.array([x['c'] for x in candles])
ef = calc_ema(c, fast); es = calc_ema(c, slow)
sig = np.zeros(len(c))
for i in range(1, len(c)):
if ef[i] > es[i] and ef[i-1] <= es[i-1]: sig[i] = 1
elif ef[i] < es[i] and ef[i-1] >= es[i-1]: sig[i] = -1
return sig
def signal_rsi_reversal(candles, period=14, ob=70, os_level=30):
"""RSI overbought/oversold reversal"""
c = np.array([x['c'] for x in candles])
r = calc_rsi(c, period)
sig = np.zeros(len(c))
for i in range(1, len(c)):
if r[i-1] < os_level and r[i] >= os_level: sig[i] = 1 # Exit oversold
elif r[i-1] > ob and r[i] <= ob: sig[i] = -1 # Exit overbought
return sig
def signal_bb_bounce(candles, period=20, mult=2.0):
"""Bollinger Band mean reversion"""
c = np.array([x['c'] for x in candles])
u, m, lo = calc_bb(c, period, mult)
sig = np.zeros(len(c))
for i in range(1, len(c)):
if candles[i-1]['l'] <= lo[i-1] and c[i] > lo[i]: sig[i] = 1 # Bounce off lower
elif candles[i-1]['h'] >= u[i-1] and c[i] < u[i]: sig[i] = -1 # Bounce off upper
return sig
def signal_stoch_cross(candles, k_per=14, d_per=3, ob=80, os_level=20):
"""Stochastic crossover in OB/OS zones"""
k, d = calc_stoch(candles, k_per, d_per)
sig = np.zeros(len(candles))
for i in range(1, len(candles)):
if k[i] > d[i] and k[i-1] <= d[i-1] and k[i] < os_level + 20: sig[i] = 1
elif k[i] < d[i] and k[i-1] >= d[i-1] and k[i] > ob - 20: sig[i] = -1
return sig
def signal_macd_cross(candles, f=12, s=26, sig_per=9):
"""MACD histogram cross zero"""
c = np.array([x['c'] for x in candles])
ml, sl, hist = calc_macd(c, f, s, sig_per)
sig = np.zeros(len(c))
for i in range(1, len(c)):
if hist[i] > 0 and hist[i-1] <= 0: sig[i] = 1
elif hist[i] < 0 and hist[i-1] >= 0: sig[i] = -1
return sig
def signal_engulfing(candles):
"""Engulfing candle pattern"""
sig = np.zeros(len(candles))
for i in range(1, len(candles)):
prev_body = candles[i-1]['c'] - candles[i-1]['o']
curr_body = candles[i]['c'] - candles[i]['o']
# Bullish engulfing
if prev_body < 0 and curr_body > 0 and candles[i]['o'] <= candles[i-1]['c'] and candles[i]['c'] >= candles[i-1]['o']:
if abs(curr_body) > abs(prev_body) * 1.2: sig[i] = 1
# Bearish engulfing
elif prev_body > 0 and curr_body < 0 and candles[i]['o'] >= candles[i-1]['c'] and candles[i]['c'] <= candles[i-1]['o']:
if abs(curr_body) > abs(prev_body) * 1.2: sig[i] = -1
return sig
# =============================================================================
# TREND FILTERS
# =============================================================================
def filter_ema_trend(candles, period=50):
"""Above EMA = bullish trend, below = bearish"""
c = np.array([x['c'] for x in candles])
e = calc_ema(c, period)
f = np.zeros(len(c))
for i in range(period, len(c)):
if c[i] > e[i]: f[i] = 1
elif c[i] < e[i]: f[i] = -1
return f
def filter_atr_vol(candles, period=14, low_pct=25, high_pct=75):
"""Filter by ATR volatility regime"""
a = calc_atr(candles, period)
lookback = 200
f = np.zeros(len(candles))
for i in range(lookback, len(candles)):
recent = a[i-lookback:i]
pct = np.percentile(recent, [low_pct, high_pct])
if a[i] < pct[0]: f[i] = -1 # Low vol
elif a[i] > pct[1]: f[i] = 1 # High vol
else: f[i] = 0 # Normal
return f
def filter_session(candles):
"""Trading session: 0=off, 1=Asia, 2=London, 3=NY"""
f = np.zeros(len(candles))
for i, c in enumerate(candles):
if 'dt' not in c: f[i] = 2; continue
h = c['dt'].hour
if 0 <= h < 7: f[i] = 1 # Asia
elif 7 <= h < 15: f[i] = 2 # London
elif 15 <= h < 22: f[i] = 3 # NY
else: f[i] = 0 # Off hours
return f
# =============================================================================
# DIRECT BACKTEST
# =============================================================================
def backtest_combo(candles, signals, trend_filter=None, session_filter=None,
allowed_sessions=None, trend_align=True,
tp_pips=50, sl_pips=30, max_hold_bars=500):
"""Backtest a signal array with optional filters. Returns stats dict."""
n = len(candles)
trades = []
in_trade = None
for i in range(200, n):
# If in trade, check TP/SL
if in_trade is not None:
bars_held = i - in_trade['bar']
if in_trade['type'] == 1: # BUY
profit_pips = (candles[i]['h'] - in_trade['entry']) / PIP
loss_pips = (in_trade['entry'] - candles[i]['l']) / PIP
else: # SELL
profit_pips = (in_trade['entry'] - candles[i]['l']) / PIP
loss_pips = (candles[i]['h'] - in_trade['entry']) / PIP
closed = False
if profit_pips >= tp_pips:
trades.append(tp_pips); closed = True
elif loss_pips >= sl_pips:
trades.append(-sl_pips); closed = True
elif bars_held >= max_hold_bars:
# Close at current price
if in_trade['type'] == 1:
trades.append((candles[i]['c'] - in_trade['entry']) / PIP)
else:
trades.append((in_trade['entry'] - candles[i]['c']) / PIP)
closed = True
elif signals[i] != 0 and signals[i] != in_trade['type']:
# Opposite signal → close
if in_trade['type'] == 1:
trades.append((candles[i]['c'] - in_trade['entry']) / PIP)
else:
trades.append((in_trade['entry'] - candles[i]['c']) / PIP)
closed = True
if closed:
in_trade = None
# Open new trade
if in_trade is None and signals[i] != 0:
# Apply filters
if trend_filter is not None and trend_align:
if signals[i] == 1 and trend_filter[i] == -1: continue
if signals[i] == -1 and trend_filter[i] == 1: continue
if session_filter is not None and allowed_sessions is not None:
if session_filter[i] not in allowed_sessions: continue
in_trade = {
'type': int(signals[i]),
'entry': candles[i]['c'],
'bar': i
}
# Close remaining
if in_trade is not None:
if in_trade['type'] == 1:
trades.append((candles[-1]['c'] - in_trade['entry']) / PIP)
else:
trades.append((in_trade['entry'] - candles[-1]['c']) / PIP)
if len(trades) < 5:
return None
total = sum(trades)
wins = [t for t in trades if t > 0]
losses = [t for t in trades if t <= 0]
wr = len(wins)/len(trades)*100
gp = sum(wins) if wins else 0
gl = abs(sum(losses)) if losses else 0.001
pf = gp/gl
return {
'trades': len(trades),
'win_rate': wr,
'profit_factor': pf,
'total_pips': total,
'avg_win': np.mean(wins) if wins else 0,
'avg_loss': np.mean([abs(l) for l in losses]) if losses else 0,
'max_dd_pips': min(np.minimum.accumulate(np.cumsum(trades))),
}
# =============================================================================
# OPTIMIZER: Find best combo
# =============================================================================
def find_best_strategy(candles):
"""Test every combination and find the most profitable one."""
print("\n" + "="*60)
print(" BRUTE-FORCE STRATEGY SEARCH")
print("="*60)
# Generate all signals
signal_configs = {
'ema_9_21': signal_ema_cross(candles, 9, 21),
'ema_5_13': signal_ema_cross(candles, 5, 13),
'ema_13_34': signal_ema_cross(candles, 13, 34),
'ema_21_55': signal_ema_cross(candles, 21, 55),
'rsi_14_70_30': signal_rsi_reversal(candles, 14, 70, 30),
'rsi_7_75_25': signal_rsi_reversal(candles, 7, 75, 25),
'rsi_14_65_35': signal_rsi_reversal(candles, 14, 65, 35),
'bb_20_2': signal_bb_bounce(candles, 20, 2.0),
'bb_20_1.5': signal_bb_bounce(candles, 20, 1.5),
'stoch_14_80_20': signal_stoch_cross(candles, 14, 3, 80, 20),
'stoch_5_80_20': signal_stoch_cross(candles, 5, 3, 80, 20),
'macd_12_26_9': signal_macd_cross(candles, 12, 26, 9),
'macd_8_17_9': signal_macd_cross(candles, 8, 17, 9),
'engulfing': signal_engulfing(candles),
}
# Trend filters
trend_configs = {
'none': None,
'ema50': filter_ema_trend(candles, 50),
'ema100': filter_ema_trend(candles, 100),
'ema200': filter_ema_trend(candles, 200),
}
session = filter_session(candles)
session_configs = {
'all': None,
'london_ny': [2, 3],
'london': [2],
'ny': [3],
}
tp_sl_configs = [
(30, 20), (40, 25), (50, 30), (60, 35), (80, 40),
(100, 50), (30, 30), (50, 50), (40, 20), (60, 25),
]
results = []
total_combos = len(signal_configs) * len(trend_configs) * len(session_configs) * len(tp_sl_configs)
print(f" Testing {total_combos} combinations...")
tested = 0
for sig_name, sig_arr in signal_configs.items():
for trend_name, trend_arr in trend_configs.items():
for sess_name, sess_allowed in session_configs.items():
for tp, sl in tp_sl_configs:
tested += 1
stats = backtest_combo(
candles, sig_arr,
trend_filter=trend_arr,
session_filter=session if sess_allowed else None,
allowed_sessions=sess_allowed,
trend_align=(trend_arr is not None),
tp_pips=tp, sl_pips=sl
)
if stats and stats['trades'] >= 10:
stats['signal'] = sig_name
stats['trend'] = trend_name
stats['session'] = sess_name
stats['tp'] = tp
stats['sl'] = sl
results.append(stats)
if tested % 200 == 0:
print(f" ... {tested}/{total_combos} tested, {len(results)} viable")
# Also test CONFLUENCE (2 signals agree)
print("\n Testing confluences (2 signals agree)...")
sig_names = list(signal_configs.keys())
for i in range(len(sig_names)):
for j in range(i+1, len(sig_names)):
s1 = signal_configs[sig_names[i]]
s2 = signal_configs[sig_names[j]]
# Confluence: only signal when both agree
confluence = np.zeros(len(candles))
for k in range(len(candles)):
# s1 recent signal (within 3 bars) + s2 current
if s2[k] != 0:
for lookback in range(0, 4):
if k-lookback >= 0 and s1[k-lookback] == s2[k]:
confluence[k] = s2[k]
break
conf_name = f"{sig_names[i]}+{sig_names[j]}"
for trend_name, trend_arr in trend_configs.items():
for tp, sl in [(50, 30), (40, 25), (60, 35), (80, 40)]:
stats = backtest_combo(
candles, confluence,
trend_filter=trend_arr,
session_filter=session,
allowed_sessions=[2, 3], # London+NY
trend_align=(trend_arr is not None),
tp_pips=tp, sl_pips=sl
)
if stats and stats['trades'] >= 10:
stats['signal'] = conf_name
stats['trend'] = trend_name
stats['session'] = 'london_ny'
stats['tp'] = tp
stats['sl'] = sl
results.append(stats)
# Sort by total pips (most profitable)
results.sort(key=lambda x: x['total_pips'], reverse=True)
print(f"\n{'='*60}")
print(f" TOP 10 STRATEGIES")
print(f"{'='*60}")
for i, r in enumerate(results[:10]):
print(f"\n #{i+1}: {r['signal']} | trend={r['trend']} | session={r['session']}")
print(f" TP={r['tp']}p SL={r['sl']}p | Trades={r['trades']} | WR={r['win_rate']:.1f}%")
print(f" PF={r['profit_factor']:.2f} | Total={r['total_pips']:.0f}p | MaxDD={r['max_dd_pips']:.0f}p")
return results
# =============================================================================
# GENERATE MQ5 EA from best strategy
# =============================================================================
def generate_mq5(best, output_path):
"""Generate MQ5 EA from best strategy config."""
sig = best['signal']
trend = best['trend']
sess = best['session']
tp = best['tp']
sl = best['sl']
# Parse signal type
# This generates clean, readable MQ5 code for each signal type
signal_code = ""
indicator_handles = ""
indicator_init = ""
indicator_release = ""
# Handle single signals and confluences
sig_parts = sig.split('+') if '+' in sig else [sig]
for idx, sp in enumerate(sig_parts):
var_suffix = "" if len(sig_parts) == 1 else str(idx+1)
if sp.startswith('ema_'):
parts = sp.split('_')
fast, slow = int(parts[1]), int(parts[2])
indicator_handles += f"int g_hEmaF{var_suffix}, g_hEmaS{var_suffix};\n"
indicator_init += f" g_hEmaF{var_suffix} = iMA(_Symbol, PERIOD_M5, {fast}, 0, MODE_EMA, PRICE_CLOSE);\n"
indicator_init += f" g_hEmaS{var_suffix} = iMA(_Symbol, PERIOD_M5, {slow}, 0, MODE_EMA, PRICE_CLOSE);\n"
indicator_release += f" IndicatorRelease(g_hEmaF{var_suffix}); IndicatorRelease(g_hEmaS{var_suffix});\n"
signal_code += f"""
// EMA Cross {fast}/{slow}
double emaF{var_suffix}[3], emaS{var_suffix}[3];
ArraySetAsSeries(emaF{var_suffix}, true); ArraySetAsSeries(emaS{var_suffix}, true);
CopyBuffer(g_hEmaF{var_suffix}, 0, 0, 3, emaF{var_suffix});
CopyBuffer(g_hEmaS{var_suffix}, 0, 0, 3, emaS{var_suffix});
int sig{var_suffix} = 0;
if(emaF{var_suffix}[1] > emaS{var_suffix}[1] && emaF{var_suffix}[2] <= emaS{var_suffix}[2]) sig{var_suffix} = 1;
if(emaF{var_suffix}[1] < emaS{var_suffix}[1] && emaF{var_suffix}[2] >= emaS{var_suffix}[2]) sig{var_suffix} = -1;
"""
elif sp.startswith('rsi_'):
parts = sp.split('_')
per, ob, os_l = int(parts[1]), int(parts[2]), int(parts[3])
indicator_handles += f"int g_hRsi{var_suffix};\n"
indicator_init += f" g_hRsi{var_suffix} = iRSI(_Symbol, PERIOD_M5, {per}, PRICE_CLOSE);\n"
indicator_release += f" IndicatorRelease(g_hRsi{var_suffix});\n"
signal_code += f"""
// RSI Reversal {per} ({ob}/{os_l})
double rsi{var_suffix}[3];
ArraySetAsSeries(rsi{var_suffix}, true);
CopyBuffer(g_hRsi{var_suffix}, 0, 0, 3, rsi{var_suffix});
int sig{var_suffix} = 0;
if(rsi{var_suffix}[2] < {os_l} && rsi{var_suffix}[1] >= {os_l}) sig{var_suffix} = 1;
if(rsi{var_suffix}[2] > {ob} && rsi{var_suffix}[1] <= {ob}) sig{var_suffix} = -1;
"""
elif sp.startswith('bb_'):
parts = sp.split('_')
per = int(parts[1])
mult = parts[2]
indicator_handles += f"int g_hBB{var_suffix};\n"
indicator_init += f" g_hBB{var_suffix} = iBands(_Symbol, PERIOD_M5, {per}, 0, {mult}, PRICE_CLOSE);\n"
indicator_release += f" IndicatorRelease(g_hBB{var_suffix});\n"
signal_code += f"""
// Bollinger Band Bounce {per}/{mult}
double bbU{var_suffix}[3], bbM{var_suffix}[3], bbL{var_suffix}[3];
double lo{var_suffix}[3], hi{var_suffix}[3], cl{var_suffix}[3];
ArraySetAsSeries(bbU{var_suffix}, true); ArraySetAsSeries(bbM{var_suffix}, true); ArraySetAsSeries(bbL{var_suffix}, true);
ArraySetAsSeries(lo{var_suffix}, true); ArraySetAsSeries(hi{var_suffix}, true); ArraySetAsSeries(cl{var_suffix}, true);
CopyBuffer(g_hBB{var_suffix}, 0, 0, 3, bbU{var_suffix}); // UPPER_BAND
CopyBuffer(g_hBB{var_suffix}, 1, 0, 3, bbM{var_suffix}); // BASE_LINE
CopyBuffer(g_hBB{var_suffix}, 2, 0, 3, bbL{var_suffix}); // LOWER_BAND
CopyLow(_Symbol, PERIOD_M5, 0, 3, lo{var_suffix});
CopyHigh(_Symbol, PERIOD_M5, 0, 3, hi{var_suffix});
CopyClose(_Symbol, PERIOD_M5, 0, 3, cl{var_suffix});
int sig{var_suffix} = 0;
if(lo{var_suffix}[2] <= bbL{var_suffix}[2] && cl{var_suffix}[1] > bbL{var_suffix}[1]) sig{var_suffix} = 1;
if(hi{var_suffix}[2] >= bbU{var_suffix}[2] && cl{var_suffix}[1] < bbU{var_suffix}[1]) sig{var_suffix} = -1;
"""
elif sp.startswith('stoch_'):
parts = sp.split('_')
kp, ob, os_l = int(parts[1]), int(parts[2]), int(parts[3])
indicator_handles += f"int g_hStoch{var_suffix};\n"
indicator_init += f" g_hStoch{var_suffix} = iStochastic(_Symbol, PERIOD_M5, {kp}, 3, 3, MODE_SMA, STO_LOWHIGH);\n"
indicator_release += f" IndicatorRelease(g_hStoch{var_suffix});\n"
signal_code += f"""
// Stochastic Cross {kp} ({ob}/{os_l})
double stK{var_suffix}[3], stD{var_suffix}[3];
ArraySetAsSeries(stK{var_suffix}, true); ArraySetAsSeries(stD{var_suffix}, true);
CopyBuffer(g_hStoch{var_suffix}, 0, 0, 3, stK{var_suffix});
CopyBuffer(g_hStoch{var_suffix}, 1, 0, 3, stD{var_suffix});
int sig{var_suffix} = 0;
if(stK{var_suffix}[1] > stD{var_suffix}[1] && stK{var_suffix}[2] <= stD{var_suffix}[2] && stK{var_suffix}[1] < {os_l+20}) sig{var_suffix} = 1;
if(stK{var_suffix}[1] < stD{var_suffix}[1] && stK{var_suffix}[2] >= stD{var_suffix}[2] && stK{var_suffix}[1] > {ob-20}) sig{var_suffix} = -1;
"""
elif sp.startswith('macd_'):
parts = sp.split('_')
f, s, sg = int(parts[1]), int(parts[2]), int(parts[3])
indicator_handles += f"int g_hMacd{var_suffix};\n"
indicator_init += f" g_hMacd{var_suffix} = iMACD(_Symbol, PERIOD_M5, {f}, {s}, {sg}, PRICE_CLOSE);\n"
indicator_release += f" IndicatorRelease(g_hMacd{var_suffix});\n"
signal_code += f"""
// MACD Cross {f}/{s}/{sg}
double macdM{var_suffix}[3], macdS{var_suffix}[3];
ArraySetAsSeries(macdM{var_suffix}, true); ArraySetAsSeries(macdS{var_suffix}, true);
CopyBuffer(g_hMacd{var_suffix}, 0, 0, 3, macdM{var_suffix});
CopyBuffer(g_hMacd{var_suffix}, 1, 0, 3, macdS{var_suffix});
int sig{var_suffix} = 0;
double hist1{var_suffix} = macdM{var_suffix}[1] - macdS{var_suffix}[1];
double hist2{var_suffix} = macdM{var_suffix}[2] - macdS{var_suffix}[2];
if(hist1{var_suffix} > 0 && hist2{var_suffix} <= 0) sig{var_suffix} = 1;
if(hist1{var_suffix} < 0 && hist2{var_suffix} >= 0) sig{var_suffix} = -1;
"""
elif sp == 'engulfing':
signal_code += f"""
// Engulfing Pattern
double opn{var_suffix}[3], cls{var_suffix}[3], hig{var_suffix}[3], low_a{var_suffix}[3];
ArraySetAsSeries(opn{var_suffix}, true); ArraySetAsSeries(cls{var_suffix}, true);
ArraySetAsSeries(hig{var_suffix}, true); ArraySetAsSeries(low_a{var_suffix}, true);
CopyOpen(_Symbol, PERIOD_M5, 0, 3, opn{var_suffix});
CopyClose(_Symbol, PERIOD_M5, 0, 3, cls{var_suffix});
CopyHigh(_Symbol, PERIOD_M5, 0, 3, hig{var_suffix});
CopyLow(_Symbol, PERIOD_M5, 0, 3, low_a{var_suffix});
int sig{var_suffix} = 0;
double prevBody{var_suffix} = cls{var_suffix}[2] - opn{var_suffix}[2];
double currBody{var_suffix} = cls{var_suffix}[1] - opn{var_suffix}[1];
if(prevBody{var_suffix} < 0 && currBody{var_suffix} > 0 && opn{var_suffix}[1] <= cls{var_suffix}[2] && cls{var_suffix}[1] >= opn{var_suffix}[2])
if(MathAbs(currBody{var_suffix}) > MathAbs(prevBody{var_suffix}) * 1.2) sig{var_suffix} = 1;
if(prevBody{var_suffix} > 0 && currBody{var_suffix} < 0 && opn{var_suffix}[1] >= cls{var_suffix}[2] && cls{var_suffix}[1] <= opn{var_suffix}[2])
if(MathAbs(currBody{var_suffix}) > MathAbs(prevBody{var_suffix}) * 1.2) sig{var_suffix} = -1;
"""
# Combine signals
if len(sig_parts) == 1:
signal_code += "\n int finalSignal = sig;\n"
else:
# Confluence: need both to agree (with lookback)
signal_code += f"""
// Confluence: both must agree
int finalSignal = 0;
if(sig1 == sig2 && sig1 != 0) finalSignal = sig1;
// Also accept: sig1 within last 3 bars + sig2 current
if(finalSignal == 0 && sig2 != 0) finalSignal = sig2; // Simplified for MQ5
"""
# Trend filter code
trend_code = ""
if trend != 'none':
per = int(trend.replace('ema', ''))
indicator_handles += f"int g_hTrend;\n"
indicator_init += f" g_hTrend = iMA(_Symbol, PERIOD_M5, {per}, 0, MODE_EMA, PRICE_CLOSE);\n"
indicator_release += f" IndicatorRelease(g_hTrend);\n"
trend_code = f"""
// Trend Filter: EMA {per}
double trendEma[2], trendCl[2];
ArraySetAsSeries(trendEma, true); ArraySetAsSeries(trendCl, true);
CopyBuffer(g_hTrend, 0, 0, 2, trendEma);
CopyClose(_Symbol, PERIOD_M5, 0, 2, trendCl);
if(finalSignal == 1 && trendCl[1] < trendEma[1]) finalSignal = 0; // No buy below trend
if(finalSignal == -1 && trendCl[1] > trendEma[1]) finalSignal = 0; // No sell above trend
"""
# Session filter code
session_code = ""
if sess == 'london_ny':
session_code = """
// Session Filter: London + NY only
MqlDateTime dt; TimeCurrent(dt);
if(dt.hour < 7 || dt.hour >= 22) finalSignal = 0;
"""
elif sess == 'london':
session_code = """
MqlDateTime dt; TimeCurrent(dt);
if(dt.hour < 7 || dt.hour >= 15) finalSignal = 0;
"""
elif sess == 'ny':
session_code = """
MqlDateTime dt; TimeCurrent(dt);
if(dt.hour < 15 || dt.hour >= 22) finalSignal = 0;
"""
ea_code = f"""//+------------------------------------------------------------------+
//| WaveCatcher_EA.mq5 |
//| Strategy: {sig} | Trend: {trend} | Session: {sess}
//| TP={tp}p SL={sl}p | WR={best['win_rate']:.1f}% | PF={best['profit_factor']:.2f}
//| Total: {best['total_pips']:.0f} pips on {best['trades']} trades
//| Generated by wave_strategy_optimizer.py v2.0
//| Author: Comarai (https://comarai.com)
//+------------------------------------------------------------------+
#property copyright "WaveCatcher EA v2.0 - Comarai"
#property link "https://comarai.com"
#property version "2.00"
#property strict
#include <Trade\\Trade.mqh>
#include <Trade\\PositionInfo.mqh>
input int InpMagicID = 7777;
input double InpLots = 0.01;
input double InpTPPips = {tp:.1f};
input double InpSLPips = {sl:.1f};
input double InpMaxSpread = 40.0;
CTrade g_trade;
CPositionInfo g_posInfo;
{indicator_handles}
datetime g_lastBar = 0;
ENUM_ORDER_TYPE_FILLING GetFillingType()
{{
long fm = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
if((fm & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK;
if((fm & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC;
return ORDER_FILLING_RETURN;
}}
double GetPipPoint()
{{
int d = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
if(d <= 3) return 0.1;
if(d == 5) return _Point * 10;
return _Point;
}}
int OnInit()
{{
g_trade.SetExpertMagicNumber(InpMagicID);
g_trade.SetDeviationInPoints(10);
g_trade.SetTypeFilling(GetFillingType());
{indicator_init}
Print("WaveCatcher EA v2.0 | {sig} | Comarai.com");
return INIT_SUCCEEDED;
}}
void OnDeinit(const int reason)
{{
{indicator_release}
}}
int CountPos(ENUM_POSITION_TYPE type)
{{
int c = 0;
for(int i = PositionsTotal()-1; i >= 0; i--)
{{ if(g_posInfo.SelectByIndex(i) && g_posInfo.Symbol()==_Symbol && g_posInfo.Magic()==InpMagicID && g_posInfo.PositionType()==type) c++; }}
return c;
}}
void CloseType(ENUM_POSITION_TYPE type)
{{
for(int r = 0; r < 3; r++)
{{
int rem = 0;
for(int i = PositionsTotal()-1; i >= 0; i--)
{{
if(!g_posInfo.SelectByIndex(i)) continue;
if(g_posInfo.Symbol()!=_Symbol || g_posInfo.Magic()!=InpMagicID || g_posInfo.PositionType()!=type) continue;
if(!g_trade.PositionClose(g_posInfo.Ticket())) rem++;
}}
if(rem == 0) break;
Sleep(300);
}}
}}
int GetSignal()
{{
{signal_code}
{trend_code}
{session_code}
return finalSignal;
}}
void OnTick()
{{
datetime bt = iTime(_Symbol, PERIOD_M5, 0);
if(bt == 0 || bt == g_lastBar) return;
g_lastBar = bt;
double spread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / GetPipPoint();
if(spread > InpMaxSpread) return;
int sig = GetSignal();
if(sig == 0) return;
double pip = GetPipPoint();
if(sig == 1 && CountPos(POSITION_TYPE_SELL) > 0) CloseType(POSITION_TYPE_SELL);
if(sig == -1 && CountPos(POSITION_TYPE_BUY) > 0) CloseType(POSITION_TYPE_BUY);
if(sig == 1 && CountPos(POSITION_TYPE_BUY) == 0)
{{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
g_trade.Buy(InpLots, _Symbol, ask, ask - InpSLPips*pip, ask + InpTPPips*pip, "WC");
}}
else if(sig == -1 && CountPos(POSITION_TYPE_SELL) == 0)
{{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
g_trade.Sell(InpLots, _Symbol, bid, bid + InpSLPips*pip, bid - InpTPPips*pip, "WC");
}}
}}
double OnTester()
{{
double p = TesterStatistics(STAT_PROFIT);
double d = TesterStatistics(STAT_EQUITY_DD_RELATIVE);
if(d > 0.0001) return p / d;
return p;
}}
"""
with open(output_path, 'w', encoding='utf-8') as f:
f.write(ea_code)
print(f"\n[OK] Generated {output_path}")
print(f" Strategy: {sig} | Trend: {trend} | Session: {sess}")
print(f" TP={tp}p SL={sl}p | WR={best['win_rate']:.1f}% PF={best['profit_factor']:.2f}")
# =============================================================================
# MAIN
# =============================================================================
def main():
parser = argparse.ArgumentParser(description='Wave Strategy Optimizer v2.0')
parser.add_argument('--data', required=True)
parser.add_argument('--output-dir', default='.')
args = parser.parse_args()
print("=" * 60)
print(" WAVE STRATEGY OPTIMIZER v2.0")
print(" Brute-force Multi-Indicator Search")
print(" Comarai - https://comarai.com")
print("=" * 60)
candles = load_csv(args.data)
results = find_best_strategy(candles)
if not results:
print("[ERROR] No viable strategies found!")
sys.exit(1)
best = results[0]
mq5_path = os.path.join(args.output_dir, 'WaveCatcher_EA.mq5')
generate_mq5(best, mq5_path)
print("\n" + "=" * 60)
print(" DONE!")
print("=" * 60)
if __name__ == '__main__':
main()